@dash-builder/react 0.0.0-stage → 0.1.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 +7 -0
- package/LICENSE +21 -0
- package/README.md +141 -2
- package/dist/context.d.ts +8 -0
- package/dist/context.js +2 -0
- package/dist/grid.d.ts +36 -0
- package/dist/grid.js +148 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/widget.d.ts +27 -0
- package/dist/widget.js +33 -0
- package/package.json +68 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @dash-builder/react
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 37f3acf: First public release: a dashboard grid where widgets flow into a fixed number of columns and can be dragged and resized, with four drop modes (insert, block, shrink, swap), drag handles, locked widgets, a row limit and a narrow (stacked) view. `@dash-builder/core` works on its own; `@dash-builder/angular` and `@dash-builder/react` wrap it.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dash-builder contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,142 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dash-builder/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React bindings for [`@dash-builder/core`](https://www.npmjs.com/package/@dash-builder/core), a dashboard grid where widgets flow left to right into a fixed number of columns and can be dragged and resized. The layout is an ordered list: array order is position, there are no x/y coordinates.
|
|
4
|
+
|
|
5
|
+
Requires React 19. Safe under `<StrictMode>` and for server rendering: the grid touches the DOM only in effects, and React does not re-render while you drag.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
`react`, `react-dom` and `@dash-builder/core` are peer dependencies, so install them together:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add @dash-builder/react @dash-builder/core react react-dom
|
|
13
|
+
# or: npm install @dash-builder/react @dash-builder/core react react-dom
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Styles
|
|
17
|
+
|
|
18
|
+
Import the grid stylesheet once, in your entry file:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import '@dash-builder/core/grid.css';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useState } from 'react';
|
|
28
|
+
import { Grid, Widget, type LayoutItem } from '@dash-builder/react';
|
|
29
|
+
import '@dash-builder/core/grid.css';
|
|
30
|
+
|
|
31
|
+
export function Dashboard() {
|
|
32
|
+
const [layout, setLayout] = useState<LayoutItem[]>([
|
|
33
|
+
{ id: 'a', w: 2, h: 2 },
|
|
34
|
+
{ id: 'b', w: 1, h: 1 },
|
|
35
|
+
{ id: 'c', w: 1, h: 1, locked: true },
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Grid columns={4} dropMode="insert" layout={layout} onLayoutChange={setLayout}>
|
|
40
|
+
{layout.map((i) => (
|
|
41
|
+
<Widget key={i.id} id={i.id}>
|
|
42
|
+
<Widget.Handle>{i.id}</Widget.Handle>
|
|
43
|
+
<p>Widget content</p>
|
|
44
|
+
</Widget>
|
|
45
|
+
))}
|
|
46
|
+
</Grid>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `<Grid>` renders the grid container (a `div` with the class `dash-grid` and `display: grid`). Each `<Widget id>` is a widget; the id must match a layout entry.
|
|
52
|
+
- `Widget.Handle` marks the only element a widget can be dragged by. Without one, the whole widget is the drag surface.
|
|
53
|
+
|
|
54
|
+
### Rules
|
|
55
|
+
|
|
56
|
+
- **The layout is controlled.** You own `layout`; update it from `onLayoutChange`. The grid never keeps its own copy. Changing `layout` from outside never calls `onLayoutChange`.
|
|
57
|
+
- **Widgets must be `<Widget>` elements that are direct DOM children of the grid.** A wrapper component is fine as long as it renders the `Widget` element itself (`function Card(p) { return <Widget id={p.id}>...</Widget>; }`), including one that shows, hides or re-ids its widget from its own state, or a `<Suspense>` boundary around widgets: every `Widget` tells the grid when it commits or unmounts, so the grid picks it up without re-rendering. An extra wrapper `<div>` between the grid and the widgets is not fine: those widgets are not found. A plain `<div data-grid-id>` is not a supported widget either.
|
|
58
|
+
- **Key widgets by id** (`key={item.id}`). The grid emits copies of the items, so keying by object identity or by index would remount widgets after every drop.
|
|
59
|
+
- **Interactive controls don't start drags.** In a widget without a handle, `input`, `textarea`, `select`, `button`, `option`, `label`, `a[href]`, `contenteditable` elements and anything inside `[data-grid-no-drag]` ignore pointer-down for dragging. For a custom control (a canvas, a map), add `data-grid-no-drag` to it, or give the widget a `Widget.Handle`. A widget root that is itself a link, label or button can still be dragged; put `data-grid-no-drag` on the root to opt out.
|
|
60
|
+
- **Locked widgets** (`locked: true`) can't be dragged or resized and keep their index.
|
|
61
|
+
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
### `<Grid>`
|
|
65
|
+
|
|
66
|
+
Extra props (`className`, `style`, `data-*`, ...) go to the container `div`. `className` is appended to `dash-grid`; `display: grid` is always applied, so `<Grid hidden>` stays visible: wrap the grid in an element you hide, or hide it with CSS (`display: none !important`).
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Default | Description |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| `layout` | `LayoutItem[]` | required | The layout, owned by the parent. |
|
|
71
|
+
| `columns` | `number` | required | Number of columns. |
|
|
72
|
+
| `onLayoutChange` | `(layout: LayoutItem[]) => void` | none | A drop or resize is released, `addItem` / `removeItem` succeed, or a `columns` change normalizes widths. |
|
|
73
|
+
| `dropMode` | `'insert' \| 'block' \| 'shrink' \| 'swap'` | `'insert'` | Changeable at runtime. |
|
|
74
|
+
| `maxRows` | `number` | none | Hard row limit for drops, resizes and `addItem`. |
|
|
75
|
+
| `rowHeight` | `number` | `80` | Row height in px. |
|
|
76
|
+
| `gap` | `number` | `8` | Gap between cells in px. |
|
|
77
|
+
| `breakpoint` | `number` | `640` | Container width in px below which the narrow view is used. |
|
|
78
|
+
| `ghost` | `(el: HTMLElement) => HTMLElement` | sanitized clone | Builds the drag ghost (see the core README). |
|
|
79
|
+
| `onReject` | `(reason: RejectReason) => void` | none | An `addItem` is refused (`'max-rows'`, `'duplicate'` or `'invalid'`). |
|
|
80
|
+
| `ref` | `Ref<GridHandle>` | none | Imperative `addItem` / `removeItem`, see below. |
|
|
81
|
+
|
|
82
|
+
### `<Widget>` and `Widget.Handle`
|
|
83
|
+
|
|
84
|
+
`<Widget id>` renders a `div` with the class `dash-item` and `data-grid-id={id}` (not a DOM `id`); `Widget.Handle` renders a `span` with `data-grid-handle`. Other props go to the element, but the grid attributes always win: `className` is appended to `dash-item`.
|
|
85
|
+
|
|
86
|
+
- Changing a widget's `className` while it is being dragged drops the core's drag-state classes (`dash-item--placeholder`, `dash-item--swap-target`, `dash-item--swap-blocked`) until the pointer moves again (swap target) or the next drag (placeholder). The drag and its drop are not affected.
|
|
87
|
+
- `dangerouslySetInnerHTML` on a `Widget` is not supported: the grid adds its own resize handle inside the widget, and React's inner HTML replaces the widget's whole content. Set it on an element inside the widget instead.
|
|
88
|
+
|
|
89
|
+
### Adding widgets: use the ref
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useRef, useState } from 'react';
|
|
93
|
+
import { Grid, Widget, type GridHandle, type LayoutItem } from '@dash-builder/react';
|
|
94
|
+
|
|
95
|
+
export function Dashboard() {
|
|
96
|
+
const grid = useRef<GridHandle>(null);
|
|
97
|
+
const [layout, setLayout] = useState<LayoutItem[]>([]);
|
|
98
|
+
|
|
99
|
+
function add() {
|
|
100
|
+
const result = grid.current!.addItem({ id: crypto.randomUUID(), w: 1, h: 1 });
|
|
101
|
+
if (!result.ok) console.warn('not added:', result.reason);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
<button type="button" onClick={add}>
|
|
107
|
+
Add
|
|
108
|
+
</button>
|
|
109
|
+
<Grid ref={grid} columns={4} layout={layout} onLayoutChange={setLayout}>
|
|
110
|
+
{layout.map((i) => (
|
|
111
|
+
<Widget key={i.id} id={i.id}>
|
|
112
|
+
{i.id}
|
|
113
|
+
</Widget>
|
|
114
|
+
))}
|
|
115
|
+
</Grid>
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| Method | Description |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| `addItem(item: LayoutItem, index?: number): DropResult` | Inserts at `index` (default: append). On success calls `onLayoutChange` with the new layout; otherwise calls `onReject` and returns `{ ok: false, reason }`. Consecutive calls in one handler build on each other. |
|
|
124
|
+
| `removeItem(id: string): void` | Removes the item and calls `onLayoutChange`; an unknown id does nothing. |
|
|
125
|
+
|
|
126
|
+
Prefer `addItem` over `setLayout([...layout, item])`: it enforces `maxRows`, refuses duplicate ids and reports the refusal through `onReject`. A plain `setLayout` bypasses all of that, because the grid shows whatever layout it is given.
|
|
127
|
+
|
|
128
|
+
## Narrow view
|
|
129
|
+
|
|
130
|
+
When the grid is narrower than `breakpoint` (default 640 px), widgets stack in a single column in list order and resizing is disabled. Dragging still reorders them, but only temporarily: a narrow-view reorder never calls `onLayoutChange`, so your layout is untouched. Set `breakpoint={0}` to turn the narrow view off. Near the edges of a scroll container, dragging autoscrolls.
|
|
131
|
+
|
|
132
|
+
## Drop modes
|
|
133
|
+
|
|
134
|
+
`insert`, `block`, `shrink` and `swap` are described in the [core README](https://www.npmjs.com/package/@dash-builder/core#drop-modes), together with locked widgets, `maxRows` and the layout JSON format.
|
|
135
|
+
|
|
136
|
+
## Demo
|
|
137
|
+
|
|
138
|
+
From the repository root: `pnpm --filter @dash-builder/core build && pnpm --filter @dash-builder/react demo`.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface GridContextValue {
|
|
2
|
+
/**
|
|
3
|
+
* Asks the grid to rescan and re-render its widgets. Calls coalesce into one refresh;
|
|
4
|
+
* outside a `<Grid>` (or during server rendering) this does nothing.
|
|
5
|
+
*/
|
|
6
|
+
requestRefresh(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare const GridContext: import("react").Context<GridContextValue>;
|
package/dist/context.js
ADDED
package/dist/grid.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode, Ref } from 'react';
|
|
2
|
+
import type { DropMode, DropResult, GridOptions, LayoutItem, RejectReason } from '@dash-builder/core';
|
|
3
|
+
export interface GridProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
4
|
+
columns: number;
|
|
5
|
+
/** The layout, owned by the parent: update it from `onLayoutChange`. */
|
|
6
|
+
layout: LayoutItem[];
|
|
7
|
+
onLayoutChange?: (layout: LayoutItem[]) => void;
|
|
8
|
+
/** Default `'insert'`. */
|
|
9
|
+
dropMode?: DropMode;
|
|
10
|
+
maxRows?: number;
|
|
11
|
+
/** Default 80 (px). */
|
|
12
|
+
rowHeight?: number;
|
|
13
|
+
/** Default 8 (px). */
|
|
14
|
+
gap?: number;
|
|
15
|
+
/** Below this container width the grid stacks in one column. Default 640 (px). */
|
|
16
|
+
breakpoint?: number;
|
|
17
|
+
ghost?: GridOptions['ghost'];
|
|
18
|
+
onReject?: (reason: RejectReason) => void;
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
ref?: Ref<GridHandle>;
|
|
21
|
+
}
|
|
22
|
+
export interface GridHandle {
|
|
23
|
+
/**
|
|
24
|
+
* Adds `item` at `index` (default: append), enforcing `maxRows`. On success
|
|
25
|
+
* `onLayoutChange` is called with the new layout; otherwise `onReject` gets the reason.
|
|
26
|
+
*/
|
|
27
|
+
addItem(item: LayoutItem, index?: number): DropResult;
|
|
28
|
+
/** Removes the item with `id`; an unknown id changes nothing and calls nothing. */
|
|
29
|
+
removeItem(id: string): void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A dashboard grid around the `@dash-builder/core` controller. The rendered div is the
|
|
33
|
+
* grid container; its direct `<Widget>` children are the widgets. Controlled: the
|
|
34
|
+
* parent owns `layout`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function Grid(props: GridProps): import("react").JSX.Element;
|
package/dist/grid.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { addItem as planAdd, createGrid, normalizeLayout, removeItem as planRemove } from '@dash-builder/core';
|
|
4
|
+
import { GridContext } from './context.js';
|
|
5
|
+
function shallowEqual(a, b) {
|
|
6
|
+
if (a === b)
|
|
7
|
+
return true;
|
|
8
|
+
if (!a || !b)
|
|
9
|
+
return false;
|
|
10
|
+
const ka = Object.keys(a);
|
|
11
|
+
return ka.length === Object.keys(b).length && ka.every((k) => Object.is(a[k], b[k]));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A dashboard grid around the `@dash-builder/core` controller. The rendered div is the
|
|
15
|
+
* grid container; its direct `<Widget>` children are the widgets. Controlled: the
|
|
16
|
+
* parent owns `layout`.
|
|
17
|
+
*/
|
|
18
|
+
export function Grid(props) {
|
|
19
|
+
const { columns, layout, onLayoutChange, dropMode = 'insert', maxRows, rowHeight = 80, gap = 8, breakpoint = 640, ghost, onReject, children, ref, className, style, ...divProps } = props;
|
|
20
|
+
const opts = { columns, dropMode, maxRows, rowHeight, gap, breakpoint, ghost };
|
|
21
|
+
const container = useRef(null);
|
|
22
|
+
const controller = useRef(undefined);
|
|
23
|
+
/** The layout last handed to the controller. */
|
|
24
|
+
const applied = useRef(undefined);
|
|
25
|
+
/** Set while setOptions runs before a new layout replaces the current one. */
|
|
26
|
+
const superseded = useRef(false);
|
|
27
|
+
/** A widget committed (or unmounted) since the controller last rendered. */
|
|
28
|
+
const dirty = useRef(false);
|
|
29
|
+
/** The className and style the container was last rendered with. */
|
|
30
|
+
const lastLook = useRef({ className, style });
|
|
31
|
+
/**
|
|
32
|
+
* The newest committed props, so callbacks never go stale and never re-create the
|
|
33
|
+
* controller. addItem/removeItem also advance `layout` to what they emitted, so calls
|
|
34
|
+
* chained before the parent re-renders compose; the next commit overwrites it.
|
|
35
|
+
*/
|
|
36
|
+
const latest = useRef({ opts, layout, onLayoutChange, onReject });
|
|
37
|
+
// Widgets call this after each of their commits and when they unmount. When the Grid
|
|
38
|
+
// commits too, its sync effect below (which runs after its children's effects) refreshes
|
|
39
|
+
// at once; otherwise (a wrapper's own state, a Suspense boundary resolving) the queued
|
|
40
|
+
// microtask does, before the browser paints. Many requests make one refresh. Once the
|
|
41
|
+
// controller is destroyed (unmount), a queued refresh finds none and does nothing.
|
|
42
|
+
const [context] = useState(() => {
|
|
43
|
+
let queued = false;
|
|
44
|
+
return {
|
|
45
|
+
requestRefresh() {
|
|
46
|
+
dirty.current = true;
|
|
47
|
+
if (queued)
|
|
48
|
+
return;
|
|
49
|
+
queued = true;
|
|
50
|
+
queueMicrotask(() => {
|
|
51
|
+
queued = false;
|
|
52
|
+
if (!dirty.current || !controller.current)
|
|
53
|
+
return;
|
|
54
|
+
dirty.current = false;
|
|
55
|
+
controller.current.refresh();
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
// Declared first, so the effects below and anything after the commit see these props.
|
|
61
|
+
useLayoutEffect(() => {
|
|
62
|
+
latest.current = { opts, layout, onLayoutChange, onReject };
|
|
63
|
+
});
|
|
64
|
+
useLayoutEffect(() => {
|
|
65
|
+
const el = container.current;
|
|
66
|
+
const { opts: o, layout: l } = latest.current;
|
|
67
|
+
applied.current = l;
|
|
68
|
+
controller.current = createGrid(el, {
|
|
69
|
+
...o,
|
|
70
|
+
layout: l,
|
|
71
|
+
onLayoutChange: (next) => {
|
|
72
|
+
if (!superseded.current)
|
|
73
|
+
latest.current.onLayoutChange?.(next);
|
|
74
|
+
},
|
|
75
|
+
onReject: (r) => latest.current.onReject?.(r),
|
|
76
|
+
});
|
|
77
|
+
dirty.current = false; // createGrid has bound and placed the widgets
|
|
78
|
+
return () => {
|
|
79
|
+
controller.current?.destroy();
|
|
80
|
+
controller.current = undefined;
|
|
81
|
+
};
|
|
82
|
+
}, []);
|
|
83
|
+
// After every commit: apply options and layout, then refresh if a widget committed or the
|
|
84
|
+
// container was restyled. The core skips equal options and layouts.
|
|
85
|
+
useLayoutEffect(() => {
|
|
86
|
+
const g = controller.current;
|
|
87
|
+
if (!g)
|
|
88
|
+
return;
|
|
89
|
+
if (layout !== applied.current) {
|
|
90
|
+
applied.current = layout;
|
|
91
|
+
// The layout re-normalized for new columns is about to be replaced: don't emit it.
|
|
92
|
+
superseded.current = true;
|
|
93
|
+
try {
|
|
94
|
+
g.setOptions(opts);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
superseded.current = false;
|
|
98
|
+
}
|
|
99
|
+
g.setLayout(layout); // normalized against the new columns; no-op on our own echo
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
g.setOptions(opts); // a columns change may normalize the layout and emit
|
|
103
|
+
}
|
|
104
|
+
// A className or style change makes React rewrite the class attribute or style keys,
|
|
105
|
+
// dropping what core set imperatively; core's render restores dash-grid--narrow and the
|
|
106
|
+
// --dash-* variables (dash-grid and display: grid are rendered by React below).
|
|
107
|
+
// dash-grid--dragging, set during a drag, is not restored: a className change mid-drag
|
|
108
|
+
// drops it until the next drag (it only disables text selection / page scroll).
|
|
109
|
+
const look = lastLook.current;
|
|
110
|
+
const restyled = className !== look.className || !shallowEqual(style, look.style);
|
|
111
|
+
lastLook.current = { className, style };
|
|
112
|
+
// Every widget added, removed or re-id'd commits (or unmounts) itself, so `dirty` covers
|
|
113
|
+
// what scanning the children's ids here would; a reorder needs no refresh (the core
|
|
114
|
+
// places widgets by layout order, not DOM order).
|
|
115
|
+
if (dirty.current || restyled) {
|
|
116
|
+
dirty.current = false;
|
|
117
|
+
g.refresh();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
useImperativeHandle(ref, () => ({
|
|
121
|
+
addItem(item, index) {
|
|
122
|
+
const { opts: o, layout: l, onLayoutChange: change, onReject: reject } = latest.current;
|
|
123
|
+
const config = { columns: o.columns, maxRows: o.maxRows, mode: o.dropMode };
|
|
124
|
+
// Plan on what the core renders (normalized), so maxRows holds for e.g. minH > h.
|
|
125
|
+
const result = planAdd(normalizeLayout(l, o.columns), config, item, index);
|
|
126
|
+
if (result.ok) {
|
|
127
|
+
latest.current.layout = result.layout; // a following add/remove builds on this
|
|
128
|
+
change?.(result.layout);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
reject?.(result.reason);
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
},
|
|
135
|
+
removeItem(id) {
|
|
136
|
+
const { layout: l, onLayoutChange: change } = latest.current;
|
|
137
|
+
const next = planRemove(l, id);
|
|
138
|
+
if (next === l)
|
|
139
|
+
return;
|
|
140
|
+
latest.current.layout = next; // a following add/remove builds on this
|
|
141
|
+
change?.(next);
|
|
142
|
+
},
|
|
143
|
+
}), []);
|
|
144
|
+
return (_jsx("div", { ...divProps,
|
|
145
|
+
// Core styles hang off dash-grid and need display: grid; rendering them here keeps them
|
|
146
|
+
// when React rewrites the class attribute or style (and applies them in SSR markup).
|
|
147
|
+
className: className ? `dash-grid ${className}` : 'dash-grid', style: { ...style, display: 'grid' }, ref: container, children: _jsx(GridContext, { value: context, children: children }) }));
|
|
148
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Grid } from './grid.js';
|
|
2
|
+
export type { GridProps, GridHandle } from './grid.js';
|
|
3
|
+
export { Widget, WidgetHandle } from './widget.js';
|
|
4
|
+
export type { WidgetProps } from './widget.js';
|
|
5
|
+
export type { LayoutItem, DropMode, RejectReason, DropResult } from '@dash-builder/core';
|
package/dist/index.js
ADDED
package/dist/widget.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef } from 'react';
|
|
2
|
+
export type WidgetProps = {
|
|
3
|
+
id: string;
|
|
4
|
+
} & Omit<ComponentPropsWithRef<'div'>, 'id'>;
|
|
5
|
+
/**
|
|
6
|
+
* A grid widget. `id` is the layout item id; it is exposed as `data-grid-id`, not as a DOM `id`.
|
|
7
|
+
*
|
|
8
|
+
* React owns the element, so a commit can undo what the core set on it imperatively:
|
|
9
|
+
* - `dash-item` is rendered here, because a `className` change rewrites the class attribute
|
|
10
|
+
* and the core adds `dash-item` only when it first binds an element. The drag-state classes
|
|
11
|
+
* (`dash-item--placeholder`, `--swap-target`, `--swap-blocked`) are set by the active drag,
|
|
12
|
+
* not by the core's render, so a `className` change in the middle of a drag drops them:
|
|
13
|
+
* the swap-target classes come back on the next pointer move, the placeholder class
|
|
14
|
+
* only with the next drag. The drag itself and its drop are unaffected.
|
|
15
|
+
* - Text children are written with `textContent`, which deletes the core's resize handle.
|
|
16
|
+
* - The widget may mount, unmount or change id without the `<Grid>` re-rendering (a wrapper
|
|
17
|
+
* with its own state, a Suspense boundary resolving).
|
|
18
|
+
* So after every commit, and when it unmounts, a widget asks the grid for a refresh: the
|
|
19
|
+
* core rescans its children and re-renders placement and handles. Requests coalesce into
|
|
20
|
+
* one refresh, which does not cancel an active drag or resize.
|
|
21
|
+
*/
|
|
22
|
+
export declare function Widget({ id, className, ...rest }: WidgetProps): import("react").JSX.Element;
|
|
23
|
+
export declare namespace Widget {
|
|
24
|
+
export { WidgetHandle as Handle };
|
|
25
|
+
}
|
|
26
|
+
/** Marks the part of a widget that starts a drag (when the grid uses handles). */
|
|
27
|
+
export declare function WidgetHandle(props: ComponentPropsWithRef<'span'>): import("react").JSX.Element;
|
package/dist/widget.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useContext, useLayoutEffect } from 'react';
|
|
3
|
+
import { GridContext } from './context.js';
|
|
4
|
+
/**
|
|
5
|
+
* A grid widget. `id` is the layout item id; it is exposed as `data-grid-id`, not as a DOM `id`.
|
|
6
|
+
*
|
|
7
|
+
* React owns the element, so a commit can undo what the core set on it imperatively:
|
|
8
|
+
* - `dash-item` is rendered here, because a `className` change rewrites the class attribute
|
|
9
|
+
* and the core adds `dash-item` only when it first binds an element. The drag-state classes
|
|
10
|
+
* (`dash-item--placeholder`, `--swap-target`, `--swap-blocked`) are set by the active drag,
|
|
11
|
+
* not by the core's render, so a `className` change in the middle of a drag drops them:
|
|
12
|
+
* the swap-target classes come back on the next pointer move, the placeholder class
|
|
13
|
+
* only with the next drag. The drag itself and its drop are unaffected.
|
|
14
|
+
* - Text children are written with `textContent`, which deletes the core's resize handle.
|
|
15
|
+
* - The widget may mount, unmount or change id without the `<Grid>` re-rendering (a wrapper
|
|
16
|
+
* with its own state, a Suspense boundary resolving).
|
|
17
|
+
* So after every commit, and when it unmounts, a widget asks the grid for a refresh: the
|
|
18
|
+
* core rescans its children and re-renders placement and handles. Requests coalesce into
|
|
19
|
+
* one refresh, which does not cancel an active drag or resize.
|
|
20
|
+
*/
|
|
21
|
+
export function Widget({ id, className, ...rest }) {
|
|
22
|
+
const { requestRefresh } = useContext(GridContext);
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
requestRefresh();
|
|
25
|
+
return requestRefresh;
|
|
26
|
+
});
|
|
27
|
+
return _jsx("div", { ...rest, className: className ? `dash-item ${className}` : 'dash-item', "data-grid-id": id });
|
|
28
|
+
}
|
|
29
|
+
/** Marks the part of a widget that starts a drag (when the grid uses handles). */
|
|
30
|
+
export function WidgetHandle(props) {
|
|
31
|
+
return _jsx("span", { ...props, "data-grid-handle": "" });
|
|
32
|
+
}
|
|
33
|
+
Widget.Handle = WidgetHandle;
|
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dash-builder/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React bindings for @dash-builder/core: a dashboard grid with draggable, resizable widgets in a fixed number of columns.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Konstantin Penzin",
|
|
7
|
+
"homepage": "https://constantine-24.github.io/dash-builder/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Constantine-24/dash-builder.git",
|
|
11
|
+
"directory": "packages/react"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Constantine-24/dash-builder/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dashboard",
|
|
18
|
+
"grid",
|
|
19
|
+
"layout",
|
|
20
|
+
"widgets",
|
|
21
|
+
"drag-and-drop",
|
|
22
|
+
"resizable",
|
|
23
|
+
"draggable",
|
|
24
|
+
"react"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"CHANGELOG.md"
|
|
40
|
+
],
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@dash-builder/core": "0.x",
|
|
43
|
+
"react": "^19.0.0",
|
|
44
|
+
"react-dom": "^19.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@dash-builder/core": "0.1.0",
|
|
48
|
+
"@testing-library/dom": "^10.4.2",
|
|
49
|
+
"@testing-library/react": "^16.3.3",
|
|
50
|
+
"@types/react": "^19.3.0",
|
|
51
|
+
"@types/react-dom": "^19.3.0",
|
|
52
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
53
|
+
"happy-dom": "^21.0.4",
|
|
54
|
+
"react": "^19.3.1",
|
|
55
|
+
"react-dom": "^19.3.1",
|
|
56
|
+
"typescript": "^7.0.2",
|
|
57
|
+
"vite": "^8.3.5",
|
|
58
|
+
"vitest": "^5.0.5"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsc -p tsconfig.build.json",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
67
|
+
"demo": "vite",
|
|
68
|
+
"build:demo": "vite build",
|
|
69
|
+
"test:smoke": "pnpm build && node scripts/smoke.mjs"
|
|
70
|
+
}
|
|
6
71
|
}
|