@particle-academy/react-fancy 5.63.0 → 5.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +1 -0
- package/dist/chunk-DTOIO42M.js +605 -0
- package/dist/chunk-DTOIO42M.js.map +1 -0
- package/dist/chunk-QP6VNKLW.cjs +608 -0
- package/dist/chunk-QP6VNKLW.cjs.map +1 -0
- package/dist/index.cjs +27 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/inputs.cjs +10 -10
- package/dist/inputs.js +2 -2
- package/dist/reorderable-grid.cjs +20 -0
- package/dist/reorderable-grid.cjs.map +1 -0
- package/dist/reorderable-grid.d.cts +141 -0
- package/dist/reorderable-grid.d.ts +141 -0
- package/dist/reorderable-grid.js +7 -0
- package/dist/reorderable-grid.js.map +1 -0
- package/docs/ReorderableGrid.md +231 -0
- package/package.json +11 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { HTMLAttributes, ReactNode, Ref, ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
/** The least an item needs. Anything else on it is yours, and reaches `renderItem` untouched. */
|
|
4
|
+
interface ReorderableGridItem {
|
|
5
|
+
/** Stable id. The order is reported as these. */
|
|
6
|
+
id: string;
|
|
7
|
+
/**
|
|
8
|
+
* Pinned: this cell keeps its slot. It cannot be picked up or dragged, and
|
|
9
|
+
* nothing can be dropped onto it; other cells flow around it.
|
|
10
|
+
*/
|
|
11
|
+
locked?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** What `renderItem` gets alongside the item. */
|
|
14
|
+
interface ReorderableGridItemState {
|
|
15
|
+
/** 0-based index in the order currently shown (the live preview while dragging). */
|
|
16
|
+
index: number;
|
|
17
|
+
/** 1-based position. */
|
|
18
|
+
position: number;
|
|
19
|
+
/** 1-based row. */
|
|
20
|
+
row: number;
|
|
21
|
+
/** 1-based column. */
|
|
22
|
+
column: number;
|
|
23
|
+
total: number;
|
|
24
|
+
/** Being dragged with a pointer. */
|
|
25
|
+
dragging: boolean;
|
|
26
|
+
/** Picked up with the keyboard. */
|
|
27
|
+
pickedUp: boolean;
|
|
28
|
+
/** This cell's slot is where the active item will land. */
|
|
29
|
+
dropTarget: boolean;
|
|
30
|
+
locked: boolean;
|
|
31
|
+
disabled: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Spread on an element to make it the drag handle. Once a cell contains a
|
|
34
|
+
* handle, only the handle starts a pointer drag; without one, the whole cell
|
|
35
|
+
* does. The keyboard always works on the cell itself.
|
|
36
|
+
*/
|
|
37
|
+
handleProps: {
|
|
38
|
+
"data-reorderable-handle": "";
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Facts every announcement can use. Positions are 1-based. */
|
|
42
|
+
interface ReorderableGridAnnouncementContext {
|
|
43
|
+
id: string;
|
|
44
|
+
/** From `getItemLabel`, else `item.label` when it is a string, else the id. */
|
|
45
|
+
label: string;
|
|
46
|
+
position: number;
|
|
47
|
+
total: number;
|
|
48
|
+
row: number;
|
|
49
|
+
column: number;
|
|
50
|
+
}
|
|
51
|
+
/** Every string the grid speaks or shows to assistive tech. Override any of them (i18n). */
|
|
52
|
+
interface ReorderableGridLabels {
|
|
53
|
+
/** Read with each cell (`aria-describedby`). */
|
|
54
|
+
instructions: string;
|
|
55
|
+
/** `aria-roledescription` of each cell. */
|
|
56
|
+
roleDescription: string;
|
|
57
|
+
pickedUp: (ctx: ReorderableGridAnnouncementContext) => string;
|
|
58
|
+
moved: (ctx: ReorderableGridAnnouncementContext) => string;
|
|
59
|
+
dropped: (ctx: ReorderableGridAnnouncementContext) => string;
|
|
60
|
+
/** `ctx` is the restored (original) position. */
|
|
61
|
+
cancelled: (ctx: ReorderableGridAnnouncementContext) => string;
|
|
62
|
+
locked: (ctx: ReorderableGridAnnouncementContext) => string;
|
|
63
|
+
}
|
|
64
|
+
interface ReorderableGridProps<T extends ReorderableGridItem> extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "defaultValue" | "onChange"> {
|
|
65
|
+
/** The cells' data. Each needs a unique `id`. */
|
|
66
|
+
items: T[];
|
|
67
|
+
/**
|
|
68
|
+
* Controlled order, as ids. Ids that are not in `items` are ignored, and
|
|
69
|
+
* items missing from it follow in `items` order, so a stale or partial order
|
|
70
|
+
* never drops a cell.
|
|
71
|
+
*/
|
|
72
|
+
value?: string[];
|
|
73
|
+
/** Uncontrolled starting order. Omit both to start in `items` order. */
|
|
74
|
+
defaultValue?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* The full new order, once per committed move: a pointer release or a
|
|
77
|
+
* keyboard drop that changed something. Never on a cancel, never on a
|
|
78
|
+
* no-op drop, never during the live preview.
|
|
79
|
+
*/
|
|
80
|
+
onReorder?: (ids: string[]) => void;
|
|
81
|
+
/** Renders a cell's content. Default: the item's label. */
|
|
82
|
+
renderItem?: (item: T, state: ReorderableGridItemState) => ReactNode;
|
|
83
|
+
/** Columns, 1 to 12. Placement is row-major. Default `2`. */
|
|
84
|
+
columns?: number;
|
|
85
|
+
/** Gutter. Default `sm`. */
|
|
86
|
+
gap?: "none" | "sm" | "md" | "lg";
|
|
87
|
+
/** No reordering at all, and no tab stops. */
|
|
88
|
+
disabled?: boolean;
|
|
89
|
+
/** Accessible name of the grid. */
|
|
90
|
+
label?: string;
|
|
91
|
+
/** The name an item is announced by. */
|
|
92
|
+
getItemLabel?: (item: T) => string;
|
|
93
|
+
/** Overrides for any announcement or label. */
|
|
94
|
+
labels?: Partial<ReorderableGridLabels>;
|
|
95
|
+
/** Skip the cell's default visuals (border, surface, padding). Behaviour stays. */
|
|
96
|
+
unstyled?: boolean;
|
|
97
|
+
/** Extra classes on every cell. */
|
|
98
|
+
cellClassName?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A grid of cells reordered in place, row-major, by pointer or keyboard.
|
|
103
|
+
*
|
|
104
|
+
* Built for field-order designers: the grid looks like the grid it designs.
|
|
105
|
+
* Order is a list of ids (`value` / `defaultValue` / `onReorder`); the pure
|
|
106
|
+
* `moveItem(ids, from, to, { locked })` is the same rule without a DOM, for
|
|
107
|
+
* agents.
|
|
108
|
+
*/
|
|
109
|
+
declare const ReorderableGrid: (<T extends ReorderableGridItem>(props: ReorderableGridProps<T> & {
|
|
110
|
+
ref?: Ref<HTMLDivElement>;
|
|
111
|
+
}) => ReactElement) & {
|
|
112
|
+
displayName?: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
interface MoveItemOptions<T> {
|
|
116
|
+
/**
|
|
117
|
+
* Pinned entries. A locked entry keeps its absolute index: it cannot be
|
|
118
|
+
* moved, nothing can be dropped onto its slot, and the other entries flow
|
|
119
|
+
* around it. Either the locked ids themselves or a predicate.
|
|
120
|
+
*/
|
|
121
|
+
locked?: Iterable<T> | ((id: T) => boolean);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Move the entry at `fromIndex` so that it ends up at `toIndex`, row-major,
|
|
125
|
+
* and return a NEW array. The input is never mutated.
|
|
126
|
+
*
|
|
127
|
+
* This is the whole reorder rule of `ReorderableGrid`, with no DOM: the grid's
|
|
128
|
+
* pointer drags and arrow keys both go through it, and an agent bridge can call
|
|
129
|
+
* it directly to reorder from a tool call.
|
|
130
|
+
*
|
|
131
|
+
* - `toIndex` is the entry's index in the RESULT, so `moveItem(ids, 0, ids.length - 1)`
|
|
132
|
+
* moves the first entry to the end.
|
|
133
|
+
* - An index outside the list (or not an integer) throws `RangeError`. That is
|
|
134
|
+
* a caller bug, and a silent no-op would hide it.
|
|
135
|
+
* - With `locked`: a locked entry keeps its absolute index and the move happens
|
|
136
|
+
* among the free slots only. Moving a locked entry, or onto a locked slot,
|
|
137
|
+
* returns the order unchanged: that is the rule doing its job, not an error.
|
|
138
|
+
*/
|
|
139
|
+
declare function moveItem<T>(ids: readonly T[], fromIndex: number, toIndex: number, options?: MoveItemOptions<T>): T[];
|
|
140
|
+
|
|
141
|
+
export { type MoveItemOptions, ReorderableGrid, type ReorderableGridAnnouncementContext, type ReorderableGridItem, type ReorderableGridItemState, type ReorderableGridLabels, type ReorderableGridProps, moveItem };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { HTMLAttributes, ReactNode, Ref, ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
/** The least an item needs. Anything else on it is yours, and reaches `renderItem` untouched. */
|
|
4
|
+
interface ReorderableGridItem {
|
|
5
|
+
/** Stable id. The order is reported as these. */
|
|
6
|
+
id: string;
|
|
7
|
+
/**
|
|
8
|
+
* Pinned: this cell keeps its slot. It cannot be picked up or dragged, and
|
|
9
|
+
* nothing can be dropped onto it; other cells flow around it.
|
|
10
|
+
*/
|
|
11
|
+
locked?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** What `renderItem` gets alongside the item. */
|
|
14
|
+
interface ReorderableGridItemState {
|
|
15
|
+
/** 0-based index in the order currently shown (the live preview while dragging). */
|
|
16
|
+
index: number;
|
|
17
|
+
/** 1-based position. */
|
|
18
|
+
position: number;
|
|
19
|
+
/** 1-based row. */
|
|
20
|
+
row: number;
|
|
21
|
+
/** 1-based column. */
|
|
22
|
+
column: number;
|
|
23
|
+
total: number;
|
|
24
|
+
/** Being dragged with a pointer. */
|
|
25
|
+
dragging: boolean;
|
|
26
|
+
/** Picked up with the keyboard. */
|
|
27
|
+
pickedUp: boolean;
|
|
28
|
+
/** This cell's slot is where the active item will land. */
|
|
29
|
+
dropTarget: boolean;
|
|
30
|
+
locked: boolean;
|
|
31
|
+
disabled: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Spread on an element to make it the drag handle. Once a cell contains a
|
|
34
|
+
* handle, only the handle starts a pointer drag; without one, the whole cell
|
|
35
|
+
* does. The keyboard always works on the cell itself.
|
|
36
|
+
*/
|
|
37
|
+
handleProps: {
|
|
38
|
+
"data-reorderable-handle": "";
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Facts every announcement can use. Positions are 1-based. */
|
|
42
|
+
interface ReorderableGridAnnouncementContext {
|
|
43
|
+
id: string;
|
|
44
|
+
/** From `getItemLabel`, else `item.label` when it is a string, else the id. */
|
|
45
|
+
label: string;
|
|
46
|
+
position: number;
|
|
47
|
+
total: number;
|
|
48
|
+
row: number;
|
|
49
|
+
column: number;
|
|
50
|
+
}
|
|
51
|
+
/** Every string the grid speaks or shows to assistive tech. Override any of them (i18n). */
|
|
52
|
+
interface ReorderableGridLabels {
|
|
53
|
+
/** Read with each cell (`aria-describedby`). */
|
|
54
|
+
instructions: string;
|
|
55
|
+
/** `aria-roledescription` of each cell. */
|
|
56
|
+
roleDescription: string;
|
|
57
|
+
pickedUp: (ctx: ReorderableGridAnnouncementContext) => string;
|
|
58
|
+
moved: (ctx: ReorderableGridAnnouncementContext) => string;
|
|
59
|
+
dropped: (ctx: ReorderableGridAnnouncementContext) => string;
|
|
60
|
+
/** `ctx` is the restored (original) position. */
|
|
61
|
+
cancelled: (ctx: ReorderableGridAnnouncementContext) => string;
|
|
62
|
+
locked: (ctx: ReorderableGridAnnouncementContext) => string;
|
|
63
|
+
}
|
|
64
|
+
interface ReorderableGridProps<T extends ReorderableGridItem> extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "defaultValue" | "onChange"> {
|
|
65
|
+
/** The cells' data. Each needs a unique `id`. */
|
|
66
|
+
items: T[];
|
|
67
|
+
/**
|
|
68
|
+
* Controlled order, as ids. Ids that are not in `items` are ignored, and
|
|
69
|
+
* items missing from it follow in `items` order, so a stale or partial order
|
|
70
|
+
* never drops a cell.
|
|
71
|
+
*/
|
|
72
|
+
value?: string[];
|
|
73
|
+
/** Uncontrolled starting order. Omit both to start in `items` order. */
|
|
74
|
+
defaultValue?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* The full new order, once per committed move: a pointer release or a
|
|
77
|
+
* keyboard drop that changed something. Never on a cancel, never on a
|
|
78
|
+
* no-op drop, never during the live preview.
|
|
79
|
+
*/
|
|
80
|
+
onReorder?: (ids: string[]) => void;
|
|
81
|
+
/** Renders a cell's content. Default: the item's label. */
|
|
82
|
+
renderItem?: (item: T, state: ReorderableGridItemState) => ReactNode;
|
|
83
|
+
/** Columns, 1 to 12. Placement is row-major. Default `2`. */
|
|
84
|
+
columns?: number;
|
|
85
|
+
/** Gutter. Default `sm`. */
|
|
86
|
+
gap?: "none" | "sm" | "md" | "lg";
|
|
87
|
+
/** No reordering at all, and no tab stops. */
|
|
88
|
+
disabled?: boolean;
|
|
89
|
+
/** Accessible name of the grid. */
|
|
90
|
+
label?: string;
|
|
91
|
+
/** The name an item is announced by. */
|
|
92
|
+
getItemLabel?: (item: T) => string;
|
|
93
|
+
/** Overrides for any announcement or label. */
|
|
94
|
+
labels?: Partial<ReorderableGridLabels>;
|
|
95
|
+
/** Skip the cell's default visuals (border, surface, padding). Behaviour stays. */
|
|
96
|
+
unstyled?: boolean;
|
|
97
|
+
/** Extra classes on every cell. */
|
|
98
|
+
cellClassName?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A grid of cells reordered in place, row-major, by pointer or keyboard.
|
|
103
|
+
*
|
|
104
|
+
* Built for field-order designers: the grid looks like the grid it designs.
|
|
105
|
+
* Order is a list of ids (`value` / `defaultValue` / `onReorder`); the pure
|
|
106
|
+
* `moveItem(ids, from, to, { locked })` is the same rule without a DOM, for
|
|
107
|
+
* agents.
|
|
108
|
+
*/
|
|
109
|
+
declare const ReorderableGrid: (<T extends ReorderableGridItem>(props: ReorderableGridProps<T> & {
|
|
110
|
+
ref?: Ref<HTMLDivElement>;
|
|
111
|
+
}) => ReactElement) & {
|
|
112
|
+
displayName?: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
interface MoveItemOptions<T> {
|
|
116
|
+
/**
|
|
117
|
+
* Pinned entries. A locked entry keeps its absolute index: it cannot be
|
|
118
|
+
* moved, nothing can be dropped onto its slot, and the other entries flow
|
|
119
|
+
* around it. Either the locked ids themselves or a predicate.
|
|
120
|
+
*/
|
|
121
|
+
locked?: Iterable<T> | ((id: T) => boolean);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Move the entry at `fromIndex` so that it ends up at `toIndex`, row-major,
|
|
125
|
+
* and return a NEW array. The input is never mutated.
|
|
126
|
+
*
|
|
127
|
+
* This is the whole reorder rule of `ReorderableGrid`, with no DOM: the grid's
|
|
128
|
+
* pointer drags and arrow keys both go through it, and an agent bridge can call
|
|
129
|
+
* it directly to reorder from a tool call.
|
|
130
|
+
*
|
|
131
|
+
* - `toIndex` is the entry's index in the RESULT, so `moveItem(ids, 0, ids.length - 1)`
|
|
132
|
+
* moves the first entry to the end.
|
|
133
|
+
* - An index outside the list (or not an integer) throws `RangeError`. That is
|
|
134
|
+
* a caller bug, and a silent no-op would hide it.
|
|
135
|
+
* - With `locked`: a locked entry keeps its absolute index and the move happens
|
|
136
|
+
* among the free slots only. Moving a locked entry, or onto a locked slot,
|
|
137
|
+
* returns the order unchanged: that is the rule doing its job, not an error.
|
|
138
|
+
*/
|
|
139
|
+
declare function moveItem<T>(ids: readonly T[], fromIndex: number, toIndex: number, options?: MoveItemOptions<T>): T[];
|
|
140
|
+
|
|
141
|
+
export { type MoveItemOptions, ReorderableGrid, type ReorderableGridAnnouncementContext, type ReorderableGridItem, type ReorderableGridItemState, type ReorderableGridLabels, type ReorderableGridProps, moveItem };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ReorderableGrid, moveItem } from './chunk-DTOIO42M.js';
|
|
2
|
+
import './chunk-SV5BW3IA.js';
|
|
3
|
+
import './chunk-YX6FTJ6N.js';
|
|
4
|
+
import './chunk-EDYAXGUM.js';
|
|
5
|
+
import './chunk-MMW2JGKZ.js';
|
|
6
|
+
//# sourceMappingURL=reorderable-grid.js.map
|
|
7
|
+
//# sourceMappingURL=reorderable-grid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"reorderable-grid.js"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# ReorderableGrid
|
|
2
|
+
|
|
3
|
+
A grid of cells that people reorder in place, row-major, with a pointer or the keyboard. While a cell is dragged, the other cells shift to show where it will land, wrapping across rows. Built for field-order designers: the designer looks like the grid it designs, so an admin arranges a deal's label/value cells in the same 2-to-4-column layout the deal page shows.
|
|
4
|
+
|
|
5
|
+
The order is a list of ids. The reorder rule is also exported on its own as `moveItem`, so an agent can reorder without the DOM.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { ReorderableGrid, moveItem } from "@particle-academy/react-fancy";
|
|
11
|
+
// or the subpath
|
|
12
|
+
import { ReorderableGrid, moveItem } from "@particle-academy/react-fancy/reorderable-grid";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Basic Usage
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const fields = [
|
|
19
|
+
{ id: "amount", label: "Amount", value: "$120,000" },
|
|
20
|
+
{ id: "stage", label: "Stage", value: "Negotiation" },
|
|
21
|
+
{ id: "owner", label: "Owner", value: "R. Diaz" },
|
|
22
|
+
{ id: "close", label: "Close date", value: "2026-12-01" },
|
|
23
|
+
{ id: "source", label: "Source", value: "Referral", locked: true },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const [order, setOrder] = useState(fields.map((f) => f.id));
|
|
27
|
+
|
|
28
|
+
<ReorderableGrid
|
|
29
|
+
items={fields}
|
|
30
|
+
value={order}
|
|
31
|
+
onReorder={setOrder}
|
|
32
|
+
columns={4}
|
|
33
|
+
label="Deal field order"
|
|
34
|
+
renderItem={(field) => (
|
|
35
|
+
<>
|
|
36
|
+
<div className="text-xs text-zinc-500">{field.label}</div>
|
|
37
|
+
<div className="font-medium">{field.value}</div>
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Uncontrolled: leave out `value`, optionally pass `defaultValue`, and listen to `onReorder`.
|
|
44
|
+
|
|
45
|
+
## Order: `value` is a list of ids
|
|
46
|
+
|
|
47
|
+
`items` carries the data and `value` (or `defaultValue`) carries the order. The order is not taken from `items`. This follows the kit's controlled pattern (`value` / `defaultValue`, see `useControllableState`), and it suits the two consumers:
|
|
48
|
+
|
|
49
|
+
- **A form or settings row stores ids.** A field-order setting is `["amount", "stage", ...]`. It is JSON, and the host does not have to rebuild its data array on every move.
|
|
50
|
+
- **An agent speaks ids too.** `grid_move` and `onReorder` carry the same shape.
|
|
51
|
+
|
|
52
|
+
The order is reconciled with `items` on every render. Ids that are not in `items` are ignored, and items the order does not mention follow in `items` order. A stale saved order (a field was added since it was saved) never hides a cell.
|
|
53
|
+
|
|
54
|
+
With neither `value` nor `defaultValue`, the grid starts in `items` order and keeps its own order from then on. To drive the order from outside (a "reset" button, a server push), pass `value`.
|
|
55
|
+
|
|
56
|
+
`onReorder` gets the **full new order once per committed move**: a pointer release or a keyboard drop that changed something. The live preview during a drag is never reported. A cancel is never reported, and neither is a drop back where the item started.
|
|
57
|
+
|
|
58
|
+
## Props
|
|
59
|
+
|
|
60
|
+
| Prop | Type | Default | Description |
|
|
61
|
+
|------|------|---------|-------------|
|
|
62
|
+
| `items` | `T[]` where `T extends { id: string; locked?: boolean }` | **required** | Cell data. Other fields are yours and reach `renderItem` |
|
|
63
|
+
| `value` | `string[]` | - | Controlled order (ids) |
|
|
64
|
+
| `defaultValue` | `string[]` | - | Uncontrolled starting order. Omit both for `items` order |
|
|
65
|
+
| `onReorder` | `(ids: string[]) => void` | - | Full new order, once per committed move |
|
|
66
|
+
| `renderItem` | `(item: T, state) => ReactNode` | the item's label | Cell content. See *Item state* |
|
|
67
|
+
| `columns` | `number` | `2` | Columns, 1 to 12. Placement is row-major |
|
|
68
|
+
| `gap` | `"none" \| "sm" \| "md" \| "lg"` | `"sm"` | Gutter |
|
|
69
|
+
| `disabled` | `boolean` | `false` | No reordering, no tab stops |
|
|
70
|
+
| `label` | `string` | - | Accessible name of the grid |
|
|
71
|
+
| `getItemLabel` | `(item: T) => string` | `item.label`, else the id | The name announcements use |
|
|
72
|
+
| `labels` | `Partial<ReorderableGridLabels>` | English | Override any announcement or label (i18n). See *Accessibility* |
|
|
73
|
+
| `unstyled` | `boolean` | `false` | Skip the cell's border, surface and padding. Behaviour stays |
|
|
74
|
+
| `cellClassName` | `string` | - | Extra classes on every cell |
|
|
75
|
+
| `className` | `string` | - | Classes on the root |
|
|
76
|
+
|
|
77
|
+
Other HTML attributes pass through to the root. A `ref` reaches the root element.
|
|
78
|
+
|
|
79
|
+
### `columns` is a fixed count
|
|
80
|
+
|
|
81
|
+
Unlike `Grid`, where `cols` is a ceiling that collapses on narrow screens, `columns` here is exact. In a designer the column count is the content: "row 2, column 3" has to mean the same slot on the deal page, and Up / Down move by exactly one row. If you need fewer columns on small screens, pass a different number from your own breakpoint hook.
|
|
82
|
+
|
|
83
|
+
### Item state
|
|
84
|
+
|
|
85
|
+
`renderItem` receives `(item, state)`:
|
|
86
|
+
|
|
87
|
+
| Field | Description |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `index` | 0-based index in the order shown (the live preview while dragging) |
|
|
90
|
+
| `position`, `row`, `column`, `total` | 1-based position, row and column, and the count |
|
|
91
|
+
| `dragging` | Being dragged with a pointer |
|
|
92
|
+
| `pickedUp` | Picked up with the keyboard |
|
|
93
|
+
| `dropTarget` | This slot is where the active item will land |
|
|
94
|
+
| `locked`, `disabled` | As passed |
|
|
95
|
+
| `handleProps` | Spread on an element to make it the drag handle |
|
|
96
|
+
|
|
97
|
+
## Pointer
|
|
98
|
+
|
|
99
|
+
Press a cell and move more than 4px to start dragging. A press without movement stays a click, so buttons and links inside cells still work. A press on a field, button or link inside a cell never starts a drag.
|
|
100
|
+
|
|
101
|
+
The dragged cell follows the pointer. The cell under the pointer is the target: the nearest cell wins, so a point in a gutter belongs to the closer neighbour. Releasing commits the move.
|
|
102
|
+
|
|
103
|
+
- **Release outside the grid to cancel.** While the pointer is outside, the preview shows the original order and the cell is dimmed.
|
|
104
|
+
- **Escape** cancels a drag in progress.
|
|
105
|
+
- The drag uses pointer capture (on the grid root), and text selection is off while dragging.
|
|
106
|
+
- **Auto-scroll:** dragging within 48px of the edge of the nearest scrolling container (or the window) scrolls it.
|
|
107
|
+
|
|
108
|
+
### Drag handles
|
|
109
|
+
|
|
110
|
+
By default the whole cell is the handle. To use a grip instead, spread `state.handleProps` on it, or put `data-reorderable-handle` on any element inside the cell. Once a cell contains a handle, only the handle starts a pointer drag.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<ReorderableGrid
|
|
114
|
+
items={fields}
|
|
115
|
+
columns={3}
|
|
116
|
+
renderItem={(field, state) => (
|
|
117
|
+
<div className="flex items-start gap-2">
|
|
118
|
+
<span {...state.handleProps} className="cursor-grab text-zinc-400" aria-hidden>
|
|
119
|
+
⠿
|
|
120
|
+
</span>
|
|
121
|
+
<div>{field.label}</div>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
/>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Cells have `touch-action: none`, so touch drags work. On a touch screen that also means a swipe that starts on a cell does not scroll the page.
|
|
128
|
+
|
|
129
|
+
## Keyboard
|
|
130
|
+
|
|
131
|
+
The grid is one tab stop (roving tabindex). With nothing picked up, the arrow keys move focus between cells in 2-D, and Home / End go to the first and last cell.
|
|
132
|
+
|
|
133
|
+
| Key | Picked up | Not picked up |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| Space / Enter | Drop here | Pick up the focused cell |
|
|
136
|
+
| ArrowLeft / ArrowRight | Move one slot | Move focus |
|
|
137
|
+
| ArrowUp / ArrowDown | Move one row | Move focus |
|
|
138
|
+
| Home / End | Move to the first / last free slot | Focus the first / last cell |
|
|
139
|
+
| Escape | Cancel and restore the original order | - |
|
|
140
|
+
|
|
141
|
+
ArrowDown from a row above a short last row lands on the last cell. Moves stop at the edges.
|
|
142
|
+
|
|
143
|
+
Escape goes through the kit's layer stack (`useEscapeKey`), so a picked-up cell inside a `Modal` or a full-screen surface cancels the pick-up first. The next Escape closes the Modal.
|
|
144
|
+
|
|
145
|
+
## Locked cells
|
|
146
|
+
|
|
147
|
+
`locked: true` pins a cell to its slot. The rule:
|
|
148
|
+
|
|
149
|
+
- A locked cell cannot be picked up or dragged. Trying announces "*Label* is locked and cannot be moved".
|
|
150
|
+
- Nothing can be dropped onto a locked slot. A pointer over one leaves the target where it was.
|
|
151
|
+
- Other cells flow around locked cells. A move happens among the free slots only, so every locked cell keeps its absolute index.
|
|
152
|
+
- Arrow keys skip pinned slots. Left / Right go on to the next free slot. Up / Down take the nearest free slot after the pinned one in reading order (forward for Down, backward for Up).
|
|
153
|
+
|
|
154
|
+
Locked cells render with a dashed border and a muted surface.
|
|
155
|
+
|
|
156
|
+
## `moveItem`
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
moveItem<T>(ids: readonly T[], fromIndex: number, toIndex: number, options?: { locked?: Iterable<T> | ((id: T) => boolean) }): T[]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The grid's reorder rule, without a DOM. It returns a new array and never mutates the input. `toIndex` is the index in the result.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
moveItem(["a", "b", "c", "d"], 0, 3); // ["b", "c", "d", "a"]
|
|
166
|
+
moveItem(["a", "b", "c", "d", "e"], 0, 3, { locked: ["c"] }); // ["b", "d", "c", "a", "e"]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- An index outside the list, or one that is not an integer, throws `RangeError`.
|
|
170
|
+
- Moving a locked entry, or onto a locked slot, returns the order unchanged.
|
|
171
|
+
|
|
172
|
+
## Human+ (agent bridge)
|
|
173
|
+
|
|
174
|
+
The grid is controlled, its cells carry stable handles, and the reorder rule is a pure function, so an agent can drive it without scraping the DOM. No bridge ships yet. A `registerReorderableGridBridge` in agent-integrations would look like this:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
registerReorderableGridBridge(server, {
|
|
178
|
+
adapter: {
|
|
179
|
+
list: () => fields.map((f, i) => ({ id: f.id, label: f.label, index: i, locked: !!f.locked })),
|
|
180
|
+
setOrder: (ids) => setOrder(ids), // the same setter onReorder uses
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
// grid_list -> [{ id, label, index, row, column, locked }]
|
|
184
|
+
// grid_move { id, toIndex } | { id, before } | { id, after }
|
|
185
|
+
// -> moveItem(order, order.indexOf(id), toIndex, { locked }), then setOrder.
|
|
186
|
+
// Returns the new order, or an error for a locked item or slot.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
A host that wants agents to propose rather than apply can stage the `grid_move` result and apply it on a human confirm.
|
|
190
|
+
|
|
191
|
+
## Accessibility
|
|
192
|
+
|
|
193
|
+
- Each cell is `role="button"` with `aria-roledescription="reorderable item"`, `aria-pressed` while active, and `aria-describedby` pointing at the instructions. Locked cells and a disabled grid set `aria-disabled`.
|
|
194
|
+
- An `aria-live="assertive"` region announces each step. Default English messages:
|
|
195
|
+
|
|
196
|
+
| Event | Message |
|
|
197
|
+
|---|---|
|
|
198
|
+
| Pick-up | Picked up Amount at position 1 of 8, row 1 column 1. Arrow keys move it, Space or Enter drops it, Escape cancels |
|
|
199
|
+
| Move | Moved to position 3 of 8, row 1 column 3 |
|
|
200
|
+
| Drop | Dropped Amount at position 3 of 8, row 1 column 3 |
|
|
201
|
+
| Cancel | Cancelled. Amount returned to position 1 of 8, row 1 column 1 |
|
|
202
|
+
| Locked | Source is locked and cannot be moved |
|
|
203
|
+
|
|
204
|
+
Override any of them through `labels`. Each message is a function of `{ id, label, position, total, row, column }`. `instructions` and `roleDescription` are plain strings.
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
<ReorderableGrid
|
|
208
|
+
items={fields}
|
|
209
|
+
columns={4}
|
|
210
|
+
labels={{
|
|
211
|
+
moved: (c) => `Position ${c.position} von ${c.total}, Zeile ${c.row} Spalte ${c.column}`,
|
|
212
|
+
instructions: "Leertaste zum Aufnehmen, Pfeiltasten zum Verschieben, Escape zum Abbrechen.",
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- Focus stays on the moving cell while it moves, and it returns there after a drop or cancel.
|
|
218
|
+
- Cell motion is skipped under `prefers-reduced-motion: reduce`.
|
|
219
|
+
|
|
220
|
+
## Styling
|
|
221
|
+
|
|
222
|
+
Cells use the kit tokens (`--fancy-surface`, `--fancy-border`, `--fancy-text`, `--fancy-focus`, and their `-dark` pairs) with `dark:` variants. For `[data-theme="dark"]` as well as `.dark`, import `dark-variant.css` (see STYLE.md). The column count is a class, not an inline style. The only inline styles are transient: the dragged cell's transform and the other cells' shift animation, both cleared when the motion ends.
|
|
223
|
+
|
|
224
|
+
## Data attributes
|
|
225
|
+
|
|
226
|
+
| Selector | Element |
|
|
227
|
+
|---|---|
|
|
228
|
+
| `[data-react-fancy-reorderable-grid]` | Root. `data-columns`, `data-state` = `idle` / `dragging` / `picked-up`, `data-drop-index` while there is a target, `data-disabled` |
|
|
229
|
+
| `[data-react-fancy-reorderable-grid-item]` | A cell. `data-reorderable-id`, `data-index` (in the order shown), `data-state` = `idle` / `dragging` / `picked-up`, `data-locked="true"`, `data-drop-target` on the active cell when a release would land it |
|
|
230
|
+
| `[data-reorderable-handle]` | A drag handle you placed inside a cell |
|
|
231
|
+
| `[data-react-fancy-reorderable-grid-live]` | The `aria-live` region |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@particle-academy/react-fancy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.64.0",
|
|
4
4
|
"description": "React UI component library for Human+ UX — controlled, agent-bridgeable primitives",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -763,6 +763,16 @@
|
|
|
763
763
|
"default": "./dist/reason-tag.cjs"
|
|
764
764
|
}
|
|
765
765
|
},
|
|
766
|
+
"./reorderable-grid": {
|
|
767
|
+
"import": {
|
|
768
|
+
"types": "./dist/reorderable-grid.d.ts",
|
|
769
|
+
"default": "./dist/reorderable-grid.js"
|
|
770
|
+
},
|
|
771
|
+
"require": {
|
|
772
|
+
"types": "./dist/reorderable-grid.d.cts",
|
|
773
|
+
"default": "./dist/reorderable-grid.cjs"
|
|
774
|
+
}
|
|
775
|
+
},
|
|
766
776
|
"./resizable-panels": {
|
|
767
777
|
"import": {
|
|
768
778
|
"types": "./dist/resizable-panels.d.ts",
|