@bsuite/page-builder 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/README.md +13 -0
- package/dist/PageGridLayout.d.ts +5 -0
- package/dist/PageGridLayout.d.ts.map +1 -0
- package/dist/PageGridLayout.js +120 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/preferences.d.ts +4 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +35 -0
- package/dist/rescaleLayout.d.ts +8 -0
- package/dist/rescaleLayout.d.ts.map +1 -0
- package/dist/rescaleLayout.js +43 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/usePageGridLayout.d.ts +4 -0
- package/dist/usePageGridLayout.d.ts.map +1 -0
- package/dist/usePageGridLayout.js +201 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +3 -0
- package/package.json +69 -0
- package/src/styles/react-grid-layout-overrides.css +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @bsuite/page-builder
|
|
2
|
+
|
|
3
|
+
Shared BSuite responsive page-builder grid primitives.
|
|
4
|
+
|
|
5
|
+
Exports:
|
|
6
|
+
|
|
7
|
+
- `PageGridLayout`
|
|
8
|
+
- `usePageGridLayout`
|
|
9
|
+
- `rescaleLayout`
|
|
10
|
+
- `GridLayouts`, `GridLayoutItem`, `WidgetMeta`, and related types
|
|
11
|
+
|
|
12
|
+
Consumer apps must depend on the published npm package, not `workspace:*` or
|
|
13
|
+
`file:` links, because each app deploys independently.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import 'react-grid-layout/css/styles.css';
|
|
2
|
+
import 'react-resizable/css/styles.css';
|
|
3
|
+
import type { PageGridLayoutProps } from './types.js';
|
|
4
|
+
export declare function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className, isResizable, resizeHandles, addEntityWidgetEventNames, createEntityWidget, onRegisterEntityWidget, }: PageGridLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
//# sourceMappingURL=PageGridLayout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AAIA,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAGxC,OAAO,KAAK,EAAe,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAkEnE,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,OAAO,EACP,UAAU,EACV,SAAS,EACT,WAAmB,EACnB,aAAsC,EACtC,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,GACvB,EAAE,mBAAmB,2CA2RrB"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Eye, EyeOff, Layers, LayoutGrid, Plus, RotateCcw, Save, Settings2 } from 'lucide-react';
|
|
3
|
+
import React, { startTransition, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { Responsive } from 'react-grid-layout';
|
|
5
|
+
import { gridBounds, maxSize, minMaxSize, minSize } from 'react-grid-layout/core';
|
|
6
|
+
import 'react-grid-layout/css/styles.css';
|
|
7
|
+
import 'react-resizable/css/styles.css';
|
|
8
|
+
import { usePageGridLayout } from './usePageGridLayout.js';
|
|
9
|
+
import { cn } from './utils.js';
|
|
10
|
+
const DEFAULT_RESIZE_BOUNDS = { minW: 2, minH: 1, maxW: 12, maxH: 16 };
|
|
11
|
+
const DEFAULT_RESIZE_HANDLES = ['se'];
|
|
12
|
+
const DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES = ['bsu-add-entity-widget', 'crm7-add-entity-widget'];
|
|
13
|
+
const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, onRemove, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
|
|
14
|
+
return (_jsx("div", { ref: ref, className: cn('relative group', injectedClassName), style: injectedStyle, ...rest, children: _jsxs("div", { className: "h-full w-full relative", children: [isEditing && _jsx("div", { className: "drag-handle absolute inset-0 z-20 cursor-move bg-transparent" }), isEditing && (_jsx("div", { className: "absolute inset-0 z-10 pointer-events-none rounded-3xl border-2 border-transparent group-hover:border-primary/50 transition-colors bg-black/5" })), isEditing && (_jsx("div", { className: "absolute top-2 left-2 z-30 flex items-center gap-1 pointer-events-none", children: _jsx("span", { className: "text-[10px] px-1.5 py-0.5 rounded-md opacity-80 font-medium bg-muted text-muted-foreground", children: label }) })), isEditing && (_jsx("button", { className: "absolute top-2 right-2 z-30 h-6 w-6 rounded-full flex items-center justify-center bg-destructive/80 hover:bg-destructive text-white shadow transition-colors", onClick: (event) => {
|
|
15
|
+
event.stopPropagation();
|
|
16
|
+
onRemove(id);
|
|
17
|
+
}, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })), _jsx("div", { className: "h-full w-full rounded-3xl transition-all flex flex-col", style: {
|
|
18
|
+
background: 'var(--bg-panel, var(--background, var(--card, #fff)))',
|
|
19
|
+
boxShadow: 'var(--shadow-shell-glow, 0 1px 3px rgb(0 0 0 / 0.1))',
|
|
20
|
+
contain: 'paint',
|
|
21
|
+
}, children: content })] }) }));
|
|
22
|
+
}));
|
|
23
|
+
export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className, isResizable = false, resizeHandles = DEFAULT_RESIZE_HANDLES, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES, createEntityWidget, onRegisterEntityWidget, }) {
|
|
24
|
+
const { currentLayouts, layoutCols, isEditing, setIsEditing, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleCompact, handleReset, addWidget, removeWidget, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
|
|
25
|
+
pageKey,
|
|
26
|
+
defaultLayouts,
|
|
27
|
+
defaultCols,
|
|
28
|
+
layoutVersion,
|
|
29
|
+
canEditPage,
|
|
30
|
+
editorEventNames,
|
|
31
|
+
preferenceAdapter,
|
|
32
|
+
});
|
|
33
|
+
const resizeEnabled = isEditing && isResizable;
|
|
34
|
+
const resizeConstraints = useMemo(() => [
|
|
35
|
+
gridBounds,
|
|
36
|
+
minMaxSize,
|
|
37
|
+
minSize(DEFAULT_RESIZE_BOUNDS.minW, DEFAULT_RESIZE_BOUNDS.minH),
|
|
38
|
+
maxSize(DEFAULT_RESIZE_BOUNDS.maxW, DEFAULT_RESIZE_BOUNDS.maxH),
|
|
39
|
+
], []);
|
|
40
|
+
const [extraWidgetConfigs, setExtraWidgetConfigs] = useState({});
|
|
41
|
+
const extraWidgets = useMemo(() => {
|
|
42
|
+
const rendered = {};
|
|
43
|
+
if (!createEntityWidget)
|
|
44
|
+
return rendered;
|
|
45
|
+
for (const [widgetId, config] of Object.entries(extraWidgetConfigs)) {
|
|
46
|
+
rendered[widgetId] = createEntityWidget({
|
|
47
|
+
widgetId,
|
|
48
|
+
entityType: config.entityType,
|
|
49
|
+
label: config.label,
|
|
50
|
+
isEditing,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return rendered;
|
|
54
|
+
}, [createEntityWidget, extraWidgetConfigs, isEditing]);
|
|
55
|
+
const allWidgets = useMemo(() => ({ ...widgets, ...extraWidgets }), [widgets, extraWidgets]);
|
|
56
|
+
const renderableWidgetKeys = useMemo(() => new Set(Object.keys(allWidgets).filter((key) => allWidgets[key] !== null && allWidgets[key] !== undefined)), [allWidgets]);
|
|
57
|
+
const activeLayouts = useMemo(() => {
|
|
58
|
+
const filtered = { lg: [] };
|
|
59
|
+
for (const bp in currentLayouts) {
|
|
60
|
+
filtered[bp] = (currentLayouts[bp] ?? []).filter((item) => renderableWidgetKeys.has(item.i));
|
|
61
|
+
}
|
|
62
|
+
return filtered;
|
|
63
|
+
}, [currentLayouts, renderableWidgetKeys]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!createEntityWidget || typeof window === 'undefined')
|
|
66
|
+
return;
|
|
67
|
+
const handleAddEntityWidget = (event) => {
|
|
68
|
+
const detail = event.detail;
|
|
69
|
+
if (!detail?.entityType)
|
|
70
|
+
return;
|
|
71
|
+
const widgetId = `entity:${detail.entityType}`;
|
|
72
|
+
const alreadyInLayout = activeLayouts.lg?.some((item) => item.i === widgetId);
|
|
73
|
+
if (alreadyInLayout) {
|
|
74
|
+
startTransition(() => setIsEditing(true));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
onRegisterEntityWidget?.({ widgetId, entityType: detail.entityType, label: detail.label });
|
|
78
|
+
setExtraWidgetConfigs((previous) => ({
|
|
79
|
+
...previous,
|
|
80
|
+
[widgetId]: { entityType: detail.entityType, label: detail.label },
|
|
81
|
+
}));
|
|
82
|
+
addWidget(widgetId, { w: 6, h: 8, minW: 3, minH: 4 });
|
|
83
|
+
startTransition(() => setIsEditing(true));
|
|
84
|
+
};
|
|
85
|
+
for (const eventName of addEntityWidgetEventNames) {
|
|
86
|
+
window.addEventListener(eventName, handleAddEntityWidget);
|
|
87
|
+
}
|
|
88
|
+
return () => {
|
|
89
|
+
for (const eventName of addEntityWidgetEventNames) {
|
|
90
|
+
window.removeEventListener(eventName, handleAddEntityWidget);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}, [
|
|
94
|
+
activeLayouts.lg,
|
|
95
|
+
addEntityWidgetEventNames,
|
|
96
|
+
addWidget,
|
|
97
|
+
createEntityWidget,
|
|
98
|
+
onRegisterEntityWidget,
|
|
99
|
+
setIsEditing,
|
|
100
|
+
]);
|
|
101
|
+
const visibleKeys = useMemo(() => new Set((activeLayouts.lg ?? []).map((item) => item.i)), [activeLayouts.lg]);
|
|
102
|
+
const hiddenWidgetKeys = useMemo(() => [...renderableWidgetKeys].filter((key) => !visibleKeys.has(key)), [renderableWidgetKeys, visibleKeys]);
|
|
103
|
+
return (_jsxs("div", { className: className, children: [isEditing && (_jsxs("div", { className: "flex flex-col gap-3 p-4 rounded-xl shadow-lg border-2 mb-4", style: { backgroundColor: 'var(--bg-panel, var(--card, #fff))', borderColor: 'var(--accent-primary, var(--primary, #2563eb))' }, children: [_jsxs("div", { className: "flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "h-10 w-10 rounded-full flex items-center justify-center animate-pulse", style: { backgroundColor: 'var(--bg-shell-accent, rgb(37 99 235 / 0.1))' }, children: _jsx(Settings2, { className: "h-5 w-5 text-primary" }) }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-lg", style: { color: 'var(--text-heading, var(--foreground, #111827))' }, children: "Canvas Editor Active" }), _jsx("span", { className: "text-sm", style: { color: 'var(--text-secondary, var(--muted-foreground, #6b7280))' }, children: "Drag anywhere on a card to move it. Resize with the bottom-right handle." })] })] }), _jsxs("button", { type: "button", className: "shrink-0 inline-flex items-center rounded-md px-3 py-2 text-sm font-medium bg-primary text-primary-foreground shadow", onClick: () => startTransition(() => setIsEditing(false)), children: [_jsx(Save, { className: "h-4 w-4 mr-2" }), "Save & Exit"] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2 pt-2 border-t", style: { borderColor: 'var(--border-shell, var(--border, #e5e7eb))' }, children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(LayoutGrid, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Columns:" }), _jsx("input", { type: "range", min: 2, max: 24, value: layoutCols, onChange: (event) => handleColumnChange(Number(event.target.value)), className: "w-24 cursor-pointer accent-primary", "aria-label": "Column count" }), _jsx("span", { className: "text-sm font-mono w-5 text-center tabular-nums", children: layoutCols })] }), _jsx("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Column presets", children: [2, 3, 4, 6, 12].map((columnCount) => (_jsx("button", { type: "button", onClick: () => handleColumnChange(columnCount), className: "px-2 py-0.5 rounded text-xs font-medium transition-colors", style: {
|
|
104
|
+
backgroundColor: layoutCols === columnCount ? 'var(--accent-primary, var(--primary, #2563eb))' : 'var(--bg-tertiary, var(--muted, #f3f4f6))',
|
|
105
|
+
color: layoutCols === columnCount ? '#fff' : 'var(--text-secondary, var(--muted-foreground, #6b7280))',
|
|
106
|
+
border: '1px solid var(--border-shell, var(--border, #e5e7eb))',
|
|
107
|
+
}, "aria-pressed": layoutCols === columnCount, children: columnCount }, columnCount))) }), hiddenWidgetKeys.length > 0 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Eye, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Add widget:" }), _jsx("div", { className: "flex flex-wrap gap-1", children: hiddenWidgetKeys.map((key) => {
|
|
108
|
+
const meta = widgetMeta?.[key];
|
|
109
|
+
const Icon = meta?.icon;
|
|
110
|
+
return (_jsxs("button", { type: "button", onClick: () => addWidget(key, meta?.defaultSize), className: "flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium border transition-colors hover:border-primary/60 bg-muted text-muted-foreground", children: [_jsx(Plus, { className: "h-3 w-3" }), Icon && _jsx(Icon, { className: "h-3 w-3" }), meta?.label ?? key] }, key));
|
|
111
|
+
}) })] })), _jsxs("button", { type: "button", onClick: handleCompact, className: "ml-auto inline-flex items-center rounded-md border px-2 py-1 text-sm text-muted-foreground", children: [_jsx(Layers, { className: "h-4 w-4 mr-1" }), "Compact Layout"] }), _jsxs("button", { type: "button", onClick: () => setResetConfirmOpen(true), className: "inline-flex items-center rounded-md border px-2 py-1 text-sm text-muted-foreground", children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), "Reset to Default"] })] })] })), resetConfirmOpen && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4", role: "dialog", "aria-modal": "true", "aria-labelledby": "page-grid-reset-title", children: _jsxs("div", { className: "max-w-md rounded-lg bg-card p-5 shadow-lg border", children: [_jsx("h2", { id: "page-grid-reset-title", className: "text-lg font-semibold", children: "Reset page layout?" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "This will discard your current layout and restore all widgets to their default positions." }), _jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [_jsx("button", { type: "button", className: "rounded-md border px-3 py-2 text-sm", onClick: () => setResetConfirmOpen(false), children: "Cancel" }), _jsx("button", { type: "button", className: "rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground", onClick: () => {
|
|
112
|
+
handleReset();
|
|
113
|
+
setResetConfirmOpen(false);
|
|
114
|
+
}, children: "Reset to Default" })] })] }) })), _jsx("div", { ref: containerRef, className: isEditing ? 'min-h-[200px]' : '', style: { backgroundColor: isEditing ? 'rgb(0 0 0 / 0.03)' : 'transparent' }, children: _jsx("div", { style: { opacity: containerWidth > 0 ? 1 : 0 }, "aria-busy": containerWidth <= 0, children: _jsx(Responsive, { width: Math.max(containerWidth, 1), className: "layout", layouts: activeLayouts, breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }, rowHeight: 32, onLayoutChange: onLayoutChange, dragConfig: { enabled: isEditing, handle: '.drag-handle', bounded: false, cancel: '.react-resizable-handle' }, resizeConfig: { enabled: resizeEnabled, handles: resizeHandles }, constraints: resizeConstraints, compactor: activeCompactor, cols: activeCols, margin: [6, 6], children: activeLayouts.lg.map((layoutItem) => {
|
|
115
|
+
const content = allWidgets[layoutItem.i];
|
|
116
|
+
if (content === undefined || content === null)
|
|
117
|
+
return null;
|
|
118
|
+
return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: widgetMeta?.[layoutItem.i]?.label ?? layoutItem.i, onRemove: removeWidget }, layoutItem.i));
|
|
119
|
+
}) }) }) })] }));
|
|
120
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { PageGridLayout } from './PageGridLayout.js';
|
|
2
|
+
export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES } from './usePageGridLayout.js';
|
|
3
|
+
export { rescaleLayout } from './rescaleLayout.js';
|
|
4
|
+
export { useLocalPreference, defaultPreferenceAdapter } from './preferences.js';
|
|
5
|
+
export type { EntityWidgetDetail, EntityWidgetFactoryOptions, GridLayoutItem, GridLayouts, PageGridLayoutProps, PageGridPreferenceAdapter, PageGridPreferenceFactory, UsePageGridLayoutOptions, UsePageGridLayoutResult, WidgetMeta, } from './types.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAChF,YAAY,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,cAAc,EACd,WAAW,EACX,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,GACX,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { PageGridLayout } from './PageGridLayout.js';
|
|
2
|
+
export { usePageGridLayout, DEFAULT_EDITOR_EVENT_NAMES } from './usePageGridLayout.js';
|
|
3
|
+
export { rescaleLayout } from './rescaleLayout.js';
|
|
4
|
+
export { useLocalPreference, defaultPreferenceAdapter } from './preferences.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PageGridPreferenceAdapter, PageGridPreferenceFactory } from './types.js';
|
|
2
|
+
export declare function useLocalPreference<T>(key: string, fallback: T): PageGridPreferenceAdapter<T>;
|
|
3
|
+
export declare const defaultPreferenceAdapter: PageGridPreferenceFactory;
|
|
4
|
+
//# sourceMappingURL=preferences.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preferences.d.ts","sourceRoot":"","sources":["../src/preferences.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAuBvF,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,yBAAyB,CAAC,CAAC,CAAC,CAc5F;AAED,eAAO,MAAM,wBAAwB,EAAE,yBAA8C,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
const STORAGE_PREFIX = 'bsuite-page-builder:';
|
|
3
|
+
function readLocal(key, fallback) {
|
|
4
|
+
try {
|
|
5
|
+
if (typeof window === 'undefined')
|
|
6
|
+
return fallback;
|
|
7
|
+
const raw = window.localStorage.getItem(STORAGE_PREFIX + key);
|
|
8
|
+
return raw ? JSON.parse(raw) : fallback;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function writeLocal(key, value) {
|
|
15
|
+
try {
|
|
16
|
+
if (typeof window === 'undefined')
|
|
17
|
+
return;
|
|
18
|
+
window.localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Storage can be disabled or full; layout still works for the session.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function useLocalPreference(key, fallback) {
|
|
25
|
+
const [value, setStateValue] = useState(() => readLocal(key, fallback));
|
|
26
|
+
const setValue = useCallback((next) => {
|
|
27
|
+
setStateValue((previous) => {
|
|
28
|
+
const resolved = typeof next === 'function' ? next(previous) : next;
|
|
29
|
+
writeLocal(key, resolved);
|
|
30
|
+
return resolved;
|
|
31
|
+
});
|
|
32
|
+
}, [key]);
|
|
33
|
+
return { value, setValue, loaded: true };
|
|
34
|
+
}
|
|
35
|
+
export const defaultPreferenceAdapter = useLocalPreference;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GridLayouts } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rescale a grid layout from one column count to another without overlapping
|
|
4
|
+
* row siblings. Items keep their row order and wrap cleanly when the new column
|
|
5
|
+
* count is narrower.
|
|
6
|
+
*/
|
|
7
|
+
export declare function rescaleLayout(layouts: GridLayouts, fromCols: number, toCols: number): GridLayouts;
|
|
8
|
+
//# sourceMappingURL=rescaleLayout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rescaleLayout.d.ts","sourceRoot":"","sources":["../src/rescaleLayout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CA6CjG"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rescale a grid layout from one column count to another without overlapping
|
|
3
|
+
* row siblings. Items keep their row order and wrap cleanly when the new column
|
|
4
|
+
* count is narrower.
|
|
5
|
+
*/
|
|
6
|
+
export function rescaleLayout(layouts, fromCols, toCols) {
|
|
7
|
+
if (fromCols === toCols || !layouts.lg)
|
|
8
|
+
return layouts;
|
|
9
|
+
const sorted = [...layouts.lg].sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x));
|
|
10
|
+
const rowMap = new Map();
|
|
11
|
+
for (const item of sorted) {
|
|
12
|
+
if (!rowMap.has(item.y))
|
|
13
|
+
rowMap.set(item.y, []);
|
|
14
|
+
rowMap.get(item.y).push(item);
|
|
15
|
+
}
|
|
16
|
+
const result = [];
|
|
17
|
+
let currentY = 0;
|
|
18
|
+
for (const [, rowItems] of [...rowMap.entries()].sort(([a], [b]) => a - b)) {
|
|
19
|
+
const rowMaxH = Math.max(...rowItems.map((item) => item.h));
|
|
20
|
+
let x = 0;
|
|
21
|
+
for (const item of rowItems) {
|
|
22
|
+
const scaledW = Math.round((item.w * toCols) / fromCols);
|
|
23
|
+
const newMinW = item.minW
|
|
24
|
+
? Math.max(1, Math.min(toCols, Math.round((item.minW * toCols) / fromCols)))
|
|
25
|
+
: undefined;
|
|
26
|
+
const newW = Math.max(newMinW ?? 1, Math.min(scaledW, toCols));
|
|
27
|
+
if (x + newW > toCols) {
|
|
28
|
+
currentY += rowMaxH;
|
|
29
|
+
x = 0;
|
|
30
|
+
}
|
|
31
|
+
result.push({
|
|
32
|
+
...item,
|
|
33
|
+
x,
|
|
34
|
+
y: currentY,
|
|
35
|
+
w: newW,
|
|
36
|
+
...(newMinW !== undefined && { minW: newMinW }),
|
|
37
|
+
});
|
|
38
|
+
x += newW;
|
|
39
|
+
}
|
|
40
|
+
currentY += rowMaxH;
|
|
41
|
+
}
|
|
42
|
+
return { ...layouts, lg: result };
|
|
43
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { Compactor, LayoutItem, ResizeHandleAxis } from 'react-grid-layout';
|
|
3
|
+
export type GridLayoutItem = LayoutItem;
|
|
4
|
+
export interface GridLayouts {
|
|
5
|
+
lg: GridLayoutItem[];
|
|
6
|
+
[key: string]: GridLayoutItem[];
|
|
7
|
+
}
|
|
8
|
+
export interface PageGridPreferenceAdapter<T> {
|
|
9
|
+
value: T;
|
|
10
|
+
setValue: (value: T | ((previous: T) => T)) => void;
|
|
11
|
+
loaded: boolean;
|
|
12
|
+
}
|
|
13
|
+
export type PageGridPreferenceFactory = <T>(key: string, fallback: T) => PageGridPreferenceAdapter<T>;
|
|
14
|
+
export interface UsePageGridLayoutOptions {
|
|
15
|
+
pageKey: string;
|
|
16
|
+
defaultLayouts: GridLayouts;
|
|
17
|
+
defaultCols?: number;
|
|
18
|
+
layoutVersion?: number;
|
|
19
|
+
canEditPage?: boolean;
|
|
20
|
+
editorEventNames?: readonly string[];
|
|
21
|
+
preferenceAdapter?: PageGridPreferenceFactory;
|
|
22
|
+
}
|
|
23
|
+
export interface WidgetMeta {
|
|
24
|
+
label: string;
|
|
25
|
+
icon?: React.ComponentType<{
|
|
26
|
+
className?: string;
|
|
27
|
+
}>;
|
|
28
|
+
defaultSize?: {
|
|
29
|
+
w?: number;
|
|
30
|
+
h?: number;
|
|
31
|
+
minW?: number;
|
|
32
|
+
minH?: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface EntityWidgetDetail {
|
|
36
|
+
entityType: string;
|
|
37
|
+
label?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface EntityWidgetFactoryOptions extends EntityWidgetDetail {
|
|
40
|
+
widgetId: string;
|
|
41
|
+
isEditing: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface PageGridLayoutProps extends UsePageGridLayoutOptions {
|
|
44
|
+
widgets: Record<string, React.ReactNode>;
|
|
45
|
+
widgetMeta?: Record<string, WidgetMeta>;
|
|
46
|
+
className?: string;
|
|
47
|
+
isResizable?: boolean;
|
|
48
|
+
resizeHandles?: readonly ResizeHandleAxis[];
|
|
49
|
+
addEntityWidgetEventNames?: readonly string[];
|
|
50
|
+
createEntityWidget?: (options: EntityWidgetFactoryOptions) => React.ReactNode;
|
|
51
|
+
onRegisterEntityWidget?: (options: EntityWidgetDetail & {
|
|
52
|
+
widgetId: string;
|
|
53
|
+
}) => void;
|
|
54
|
+
}
|
|
55
|
+
export interface UsePageGridLayoutResult {
|
|
56
|
+
currentLayouts: GridLayouts;
|
|
57
|
+
layoutCols: number;
|
|
58
|
+
isEditing: boolean;
|
|
59
|
+
setIsEditing: React.Dispatch<React.SetStateAction<boolean>>;
|
|
60
|
+
activeCols: Record<string, number>;
|
|
61
|
+
activeCompactor: Compactor;
|
|
62
|
+
onLayoutChange: (_layout: unknown, layouts: unknown) => void;
|
|
63
|
+
handleColumnChange: (newCols: number) => void;
|
|
64
|
+
handleCompact: () => void;
|
|
65
|
+
handleReset: () => void;
|
|
66
|
+
addWidget: (widgetKey: string, initialSize?: Partial<Pick<GridLayoutItem, 'w' | 'h' | 'minW' | 'minH'>>) => void;
|
|
67
|
+
removeWidget: (widgetKey: string) => void;
|
|
68
|
+
resetConfirmOpen: boolean;
|
|
69
|
+
setResetConfirmOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
70
|
+
canEditPage: boolean;
|
|
71
|
+
containerRef: React.RefObject<HTMLElement | null>;
|
|
72
|
+
containerWidth: number;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { UsePageGridLayoutOptions, UsePageGridLayoutResult } from './types.js';
|
|
2
|
+
export declare const DEFAULT_EDITOR_EVENT_NAMES: readonly ["bsuite-open-page-editor", "bsu-open-page-editor", "crm7-open-page-editor", "conduit-open-page-editor", "r80-open-page-editor"];
|
|
3
|
+
export declare function usePageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, }: UsePageGridLayoutOptions): UsePageGridLayoutResult;
|
|
4
|
+
//# sourceMappingURL=usePageGridLayout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAGV,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,0BAA0B,2IAM7B,CAAC;AAEX,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,cAAc,EACd,WAAgB,EAChB,aAAiB,EACjB,WAAkB,EAClB,gBAA6C,EAC7C,iBAA4C,GAC7C,EAAE,wBAAwB,GAAG,uBAAuB,CA4NpD"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { startTransition, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
2
|
+
import { noCompactor, verticalCompactor } from 'react-grid-layout';
|
|
3
|
+
import { defaultPreferenceAdapter } from './preferences.js';
|
|
4
|
+
import { rescaleLayout } from './rescaleLayout.js';
|
|
5
|
+
export const DEFAULT_EDITOR_EVENT_NAMES = [
|
|
6
|
+
'bsuite-open-page-editor',
|
|
7
|
+
'bsu-open-page-editor',
|
|
8
|
+
'crm7-open-page-editor',
|
|
9
|
+
'conduit-open-page-editor',
|
|
10
|
+
'r80-open-page-editor',
|
|
11
|
+
];
|
|
12
|
+
export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, layoutVersion = 1, canEditPage = true, editorEventNames = DEFAULT_EDITOR_EVENT_NAMES, preferenceAdapter = defaultPreferenceAdapter, }) {
|
|
13
|
+
const containerRef = useRef(null);
|
|
14
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
15
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
16
|
+
const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
|
|
17
|
+
useLayoutEffect(() => {
|
|
18
|
+
const el = containerRef.current;
|
|
19
|
+
if (!el)
|
|
20
|
+
return;
|
|
21
|
+
setContainerWidth(el.offsetWidth);
|
|
22
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
23
|
+
const ro = new ResizeObserver((entries) => {
|
|
24
|
+
const entry = entries[0];
|
|
25
|
+
if (entry) {
|
|
26
|
+
const width = entry.contentRect.width;
|
|
27
|
+
setContainerWidth((previous) => (width === previous ? previous : width));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
ro.observe(el);
|
|
31
|
+
return () => ro.disconnect();
|
|
32
|
+
}
|
|
33
|
+
const onResize = () => setContainerWidth(el.offsetWidth);
|
|
34
|
+
window.addEventListener('resize', onResize);
|
|
35
|
+
return () => window.removeEventListener('resize', onResize);
|
|
36
|
+
}, []);
|
|
37
|
+
const { value: savedLayoutVersion, setValue: setSavedLayoutVersion, loaded: versionLoaded } = preferenceAdapter(`page:${pageKey}_grid_version`, 0);
|
|
38
|
+
const { value: savedLayout, setValue: setSavedLayout, loaded: layoutLoaded } = preferenceAdapter(`page:${pageKey}_grid_layouts`, defaultLayouts);
|
|
39
|
+
const { value: savedLayoutCols, setValue: setLayoutCols } = preferenceAdapter(`page:${pageKey}_grid_cols`, defaultCols);
|
|
40
|
+
const { value: savedBaseCols, setValue: setBaseCols } = preferenceAdapter(`page:${pageKey}_grid_base_cols`, defaultCols);
|
|
41
|
+
const prefsLoaded = layoutLoaded && versionLoaded;
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!prefsLoaded)
|
|
44
|
+
return;
|
|
45
|
+
if ((savedLayoutVersion ?? 0) < layoutVersion) {
|
|
46
|
+
startTransition(() => {
|
|
47
|
+
setSavedLayout(defaultLayouts);
|
|
48
|
+
setLayoutCols(defaultCols);
|
|
49
|
+
setBaseCols(defaultCols);
|
|
50
|
+
setSavedLayoutVersion(layoutVersion);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}, [
|
|
54
|
+
defaultCols,
|
|
55
|
+
defaultLayouts,
|
|
56
|
+
layoutVersion,
|
|
57
|
+
prefsLoaded,
|
|
58
|
+
savedLayoutVersion,
|
|
59
|
+
setBaseCols,
|
|
60
|
+
setLayoutCols,
|
|
61
|
+
setSavedLayout,
|
|
62
|
+
setSavedLayoutVersion,
|
|
63
|
+
]);
|
|
64
|
+
const layoutCols = Math.max(2, savedLayoutCols ?? defaultCols);
|
|
65
|
+
const baseCols = Math.max(2, savedBaseCols ?? defaultCols);
|
|
66
|
+
const rawLayouts = useMemo(() => {
|
|
67
|
+
if (Object.keys(savedLayout || {}).length === 0)
|
|
68
|
+
return defaultLayouts;
|
|
69
|
+
const saved = savedLayout;
|
|
70
|
+
let changed = false;
|
|
71
|
+
const merged = {};
|
|
72
|
+
for (const bp of Object.keys(saved)) {
|
|
73
|
+
const savedItems = saved[bp] ?? [];
|
|
74
|
+
const savedKeys = new Set(savedItems.map((item) => item.i));
|
|
75
|
+
const bpDefaults = defaultLayouts[bp] ?? defaultLayouts.lg;
|
|
76
|
+
const missingItems = bpDefaults.filter((item) => !savedKeys.has(item.i));
|
|
77
|
+
if (missingItems.length > 0) {
|
|
78
|
+
const maxY = savedItems.reduce((max, item) => Math.max(max, item.y + item.h), 0);
|
|
79
|
+
const offsetItems = missingItems.map((item, index) => ({ ...item, y: maxY + index * item.h }));
|
|
80
|
+
merged[bp] = [...savedItems, ...offsetItems];
|
|
81
|
+
changed = true;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
merged[bp] = savedItems;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return changed ? merged : savedLayout;
|
|
88
|
+
}, [savedLayout, defaultLayouts]);
|
|
89
|
+
const currentLayouts = useMemo(() => {
|
|
90
|
+
if (baseCols !== layoutCols)
|
|
91
|
+
return rescaleLayout(rawLayouts, baseCols, layoutCols);
|
|
92
|
+
return rawLayouts;
|
|
93
|
+
}, [rawLayouts, baseCols, layoutCols]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (baseCols !== layoutCols) {
|
|
96
|
+
const rescaled = rescaleLayout(rawLayouts, baseCols, layoutCols);
|
|
97
|
+
setSavedLayout(rescaled);
|
|
98
|
+
setBaseCols(layoutCols);
|
|
99
|
+
}
|
|
100
|
+
}, [baseCols, layoutCols, rawLayouts, setBaseCols, setSavedLayout]);
|
|
101
|
+
const activeCols = useMemo(() => ({ lg: layoutCols, md: layoutCols, sm: layoutCols, xs: layoutCols, xxs: layoutCols }), [layoutCols]);
|
|
102
|
+
const activeCompactor = useMemo(() => (isEditing ? { ...noCompactor, preventCollision: true } : verticalCompactor), [isEditing]);
|
|
103
|
+
const onLayoutChange = useCallback((_layout, layouts) => {
|
|
104
|
+
if (isEditing)
|
|
105
|
+
setSavedLayout(layouts);
|
|
106
|
+
}, [isEditing, setSavedLayout]);
|
|
107
|
+
const handleColumnChange = useCallback((newCols) => {
|
|
108
|
+
const rescaled = rescaleLayout(currentLayouts, layoutCols, newCols);
|
|
109
|
+
setSavedLayout(rescaled);
|
|
110
|
+
setLayoutCols(newCols);
|
|
111
|
+
setBaseCols(newCols);
|
|
112
|
+
}, [currentLayouts, layoutCols, setBaseCols, setLayoutCols, setSavedLayout]);
|
|
113
|
+
const handleCompact = useCallback(() => {
|
|
114
|
+
if (!currentLayouts.lg)
|
|
115
|
+
return;
|
|
116
|
+
const sorted = [...currentLayouts.lg].sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x));
|
|
117
|
+
const placed = [];
|
|
118
|
+
const result = sorted.map((item) => {
|
|
119
|
+
let newY = 0;
|
|
120
|
+
for (const placedItem of placed) {
|
|
121
|
+
const overlapsHorizontally = item.x < placedItem.x + placedItem.w && item.x + item.w > placedItem.x;
|
|
122
|
+
if (overlapsHorizontally)
|
|
123
|
+
newY = Math.max(newY, placedItem.y + placedItem.h);
|
|
124
|
+
}
|
|
125
|
+
const compacted = { ...item, y: newY };
|
|
126
|
+
placed.push(compacted);
|
|
127
|
+
return compacted;
|
|
128
|
+
});
|
|
129
|
+
startTransition(() => setSavedLayout({ ...currentLayouts, lg: result }));
|
|
130
|
+
}, [currentLayouts, setSavedLayout]);
|
|
131
|
+
const handleReset = useCallback(() => {
|
|
132
|
+
startTransition(() => {
|
|
133
|
+
setSavedLayout(defaultLayouts);
|
|
134
|
+
setLayoutCols(defaultCols);
|
|
135
|
+
setBaseCols(defaultCols);
|
|
136
|
+
});
|
|
137
|
+
}, [defaultCols, defaultLayouts, setBaseCols, setLayoutCols, setSavedLayout]);
|
|
138
|
+
const addWidget = useCallback((widgetKey, initialSize) => {
|
|
139
|
+
const bps = Object.keys(currentLayouts);
|
|
140
|
+
const maxY = (currentLayouts.lg ?? []).reduce((max, item) => Math.max(max, item.y + item.h), 0);
|
|
141
|
+
const newItem = {
|
|
142
|
+
i: widgetKey,
|
|
143
|
+
x: 0,
|
|
144
|
+
y: maxY,
|
|
145
|
+
w: initialSize?.w ?? Math.min(6, layoutCols),
|
|
146
|
+
h: initialSize?.h ?? 8,
|
|
147
|
+
minW: initialSize?.minW,
|
|
148
|
+
minH: initialSize?.minH,
|
|
149
|
+
};
|
|
150
|
+
const updated = { lg: [] };
|
|
151
|
+
for (const bp of bps) {
|
|
152
|
+
updated[bp] = [...(currentLayouts[bp] ?? []), newItem];
|
|
153
|
+
}
|
|
154
|
+
startTransition(() => setSavedLayout(updated));
|
|
155
|
+
}, [currentLayouts, layoutCols, setSavedLayout]);
|
|
156
|
+
const removeWidget = useCallback((widgetKey) => {
|
|
157
|
+
const updated = { lg: [] };
|
|
158
|
+
for (const bp of Object.keys(currentLayouts)) {
|
|
159
|
+
updated[bp] = (currentLayouts[bp] ?? []).filter((item) => item.i !== widgetKey);
|
|
160
|
+
}
|
|
161
|
+
startTransition(() => setSavedLayout(updated));
|
|
162
|
+
}, [currentLayouts, setSavedLayout]);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!canEditPage || typeof window === 'undefined')
|
|
165
|
+
return;
|
|
166
|
+
const handler = (event) => {
|
|
167
|
+
const detail = event.detail;
|
|
168
|
+
if (!detail?.page || detail.page === pageKey) {
|
|
169
|
+
detail?.ack?.();
|
|
170
|
+
setIsEditing(true);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
for (const eventName of editorEventNames) {
|
|
174
|
+
window.addEventListener(eventName, handler);
|
|
175
|
+
}
|
|
176
|
+
return () => {
|
|
177
|
+
for (const eventName of editorEventNames) {
|
|
178
|
+
window.removeEventListener(eventName, handler);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}, [canEditPage, editorEventNames, pageKey]);
|
|
182
|
+
return {
|
|
183
|
+
currentLayouts,
|
|
184
|
+
layoutCols,
|
|
185
|
+
isEditing,
|
|
186
|
+
setIsEditing,
|
|
187
|
+
activeCols,
|
|
188
|
+
activeCompactor,
|
|
189
|
+
onLayoutChange,
|
|
190
|
+
handleColumnChange,
|
|
191
|
+
handleCompact,
|
|
192
|
+
handleReset,
|
|
193
|
+
addWidget,
|
|
194
|
+
removeWidget,
|
|
195
|
+
resetConfirmOpen,
|
|
196
|
+
setResetConfirmOpen,
|
|
197
|
+
canEditPage,
|
|
198
|
+
containerRef,
|
|
199
|
+
containerWidth,
|
|
200
|
+
};
|
|
201
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,EAAE,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,MAAM,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,CAE/E"}
|
package/dist/utils.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bsuite/page-builder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared BSuite responsive page-builder grid and layout persistence primitives",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/GaryOcean428/bsuite.git",
|
|
13
|
+
"directory": "packages/page-builder"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src/styles"
|
|
18
|
+
],
|
|
19
|
+
"main": "dist/index.js",
|
|
20
|
+
"types": "dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts"
|
|
25
|
+
},
|
|
26
|
+
"./styles.css": "./src/styles/react-grid-layout-overrides.css"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"prepublishOnly": "pnpm build"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@dnd-kit/core": "^6",
|
|
36
|
+
"@dnd-kit/sortable": "^10",
|
|
37
|
+
"@dnd-kit/utilities": "^3",
|
|
38
|
+
"lucide-react": ">=0.400.0",
|
|
39
|
+
"react": ">=18 <21",
|
|
40
|
+
"react-dom": ">=18 <21",
|
|
41
|
+
"react-grid-layout": "^2",
|
|
42
|
+
"react-resizable": "^3"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"@dnd-kit/core": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"@dnd-kit/sortable": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@dnd-kit/utilities": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
57
|
+
"@testing-library/react": "^16.3.2",
|
|
58
|
+
"@types/react": "^18.3.28",
|
|
59
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
60
|
+
"jsdom": "^28.1.0",
|
|
61
|
+
"lucide-react": "^0.576.0",
|
|
62
|
+
"react": "^18.3.1",
|
|
63
|
+
"react-dom": "^18.3.1",
|
|
64
|
+
"react-grid-layout": "^2.2.2",
|
|
65
|
+
"react-resizable": "^3.0.5",
|
|
66
|
+
"typescript": "~5.7.3",
|
|
67
|
+
"vitest": "^3.2.4"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.react-grid-layout {
|
|
2
|
+
position: relative;
|
|
3
|
+
transition: height 200ms ease;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.react-grid-item {
|
|
7
|
+
transition: transform 200ms ease, width 200ms ease, height 200ms ease;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.react-grid-item.react-grid-placeholder {
|
|
11
|
+
border-radius: 1.5rem;
|
|
12
|
+
background: var(--accent-primary, var(--primary, #2563eb));
|
|
13
|
+
opacity: 0.16;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.react-grid-item > .react-resizable-handle {
|
|
17
|
+
z-index: 35;
|
|
18
|
+
}
|