-
-
Notifications
You must be signed in to change notification settings - Fork 9.9k
React Grid Layout in shadcn #10466
Prajwal-alphanimble
started this conversation in
Ideas
React Grid Layout in shadcn
#10466
Replies: 1 comment · 5 replies
|
Consider using snapgrid. React Grid Layout is now obsolete. |
All reactions
5 replies
|
alright, i guess snapgrid is better, thank you. |
All reactions
-
❤️ 1
|
I have a really crude and shitty implementation. Kinda want to add this to the components registry; I have no idea how to contribute, unfortunately. "use client";
/**
* widget-grid.tsx
*
* All-in-one grid system — shadcn/ui style.
*
* ─── Widget Grid ──────────────────────────────────────────────────────────────
* <WidgetGrid> The grid. Pass layout + onLayoutChange, done.
*
* ─── Widget shell ─────────────────────────────────────────────────────────────
* <Widget> Card wrapper (bg-card, flex-col, fills cell)
* <WidgetHeader> Drag bar — auto-attaches handleRef from context
* <WidgetGrip> GripVertical icon
* <WidgetTitle> flex-1 truncating heading
* <WidgetActions> Right-side slot + auto-wired close button
* <WidgetBody> Scrollable content (padding="default"|"flush")
* <WidgetSeparator> <hr> divider
*
* ─── Skeleton ─────────────────────────────────────────────────────────────────
* <WidgetGridSkeleton> CSS-grid skeleton matching Grid's config
* <WidgetGridSkeletonCard> Single skeleton card (header + pulse body)
* <WidgetGridSkeletonPulse> Animated muted pulse + loading pill
*
* ─── Hooks ────────────────────────────────────────────────────────────────────
* useRemoveWidget() Remove the current widget from the grid
* useWidgetDragHandle() The drag-handle ref for the current widget
* useGridDragging() True while any widget is being dragged
* useGridJustDropped() True for one frame after drop (animation guard)
*
* ─── Usage ────────────────────────────────────────────────────────────────────
* <Grid layout={layout} onLayoutChange={setLayout}>
* <Widget key="pnl">
* <WidgetHeader>
* <WidgetGrip />
* <WidgetTitle>P&L</WidgetTitle>
* <WidgetActions />
* </WidgetHeader>
* <WidgetBody>
* <PnlCard />
* </WidgetBody>
* </Widget>
* </Grid>
*/
import * as React from "react";
import { DragDropProvider } from "@dnd-kit/react";
import {
removeItemWithCompactor,
useContainerWidth,
useGridContainer,
useGridItem,
useGridPlaceholder,
useGridResizeHandle,
verticalCompactor,
} from "@snapgridjs/react";
import type { Layout, LayoutItem } from "@snapgridjs/react";
import { GripVertical, LoaderCircle, X } from "lucide-react";
import { cn } from "~/lib/utils";
// ─────────────────────────────────────────────────────────────────────────────
// § 1. Grid config constants
// All skeleton math derives from these — change here, skeleton follows.
// ─────────────────────────────────────────────────────────────────────────────
export const GRID_COLS = 12;
export const GRID_ROW_HEIGHT = 36; // px
export const GRID_MARGIN = 16; // px, both axes
const CARD_RADIUS_PX = 8;
const CARD_RADIUS_CLASS = "rounded-lg";
const RESIZE_HANDLE_SIZE = 8;
const RESIZE_ARM = 5;
const CORNER_ZONE = 8;
const resizePath = (() => {
const s = RESIZE_HANDLE_SIZE,
r = CARD_RADIUS_PX,
a = RESIZE_ARM;
return `M ${s} ${s - r - a} L ${s} ${s - r} A ${r} ${r} 0 0 1 ${s - r} ${s} L ${s - r - a} ${s}`;
})();
// ─────────────────────────────────────────────────────────────────────────────
// § 2. Internal contexts
// ─────────────────────────────────────────────────────────────────────────────
const RemoveWidgetContext = React.createContext<((id: string) => void) | null>(null);
const WidgetIdContext = React.createContext<string | null>(null);
/**
* The dnd-kit drag-handle ref for the nearest Grid item.
* WidgetHeader consumes this automatically — export for edge cases.
*/
export const WidgetDragHandleRefContext = React.createContext<React.Ref<HTMLElement> | null>(null);
const GridDragStateContext = React.createContext<{
isDragging: boolean;
justDropped: boolean;
}>({ isDragging: false, justDropped: false });
// ─────────────────────────────────────────────────────────────────────────────
// § 3. Public hooks
// ─────────────────────────────────────────────────────────────────────────────
/** Returns a callback that removes the current widget, or undefined outside a grid. */
export function useRemoveWidget() {
const id = React.useContext(WidgetIdContext);
const remove = React.useContext(RemoveWidgetContext);
return id && remove ? () => remove(id) : undefined;
}
/** The drag-handle ref for the current widget. Attach to your drag zone. */
export function useWidgetDragHandle(): React.Ref<HTMLElement> | null {
return React.useContext(WidgetDragHandleRefContext);
}
/** True while any widget is being dragged. */
export function useGridDragging(): boolean {
return React.useContext(GridDragStateContext).isDragging;
}
/**
* True for one frame after a drag ends. Use to suppress AnimatePresence
* re-mount animations on components that unmount/remount during a drop.
*/
export function useGridJustDropped(): boolean {
return React.useContext(GridDragStateContext).justDropped;
}
// ─────────────────────────────────────────────────────────────────────────────
// § 4. Ref merge utility (React 19 compatible)
// ─────────────────────────────────────────────────────────────────────────────
function assignRef<T>(ref: React.Ref<T> | undefined | null, el: T | null) {
if (!ref) return;
if (typeof ref === "function") {
ref(el);
} else {
(ref as React.RefObject<T | null>).current = el;
}
}
function useMergedRef<T>(...refs: (React.Ref<T> | undefined | null)[]): React.RefCallback<T> {
const stored = React.useRef<(React.Ref<T> | undefined | null)[]>(refs);
// eslint-disable-next-line react-hooks/refs
stored.current = refs;
return React.useCallback((el: T | null) => {
stored.current.forEach((r) => assignRef(r, el));
}, []);
}
// ─────────────────────────────────────────────────────────────────────────────
// § 5. Internal grid item
// ─────────────────────────────────────────────────────────────────────────────
function GridItemOverlay({
isResizing,
isDragging,
isInteractive,
}: {
isResizing: boolean;
isDragging: boolean;
isInteractive: boolean;
}) {
if (!isResizing && !isDragging && !isInteractive) return null;
return (
<div
className={cn(
"pointer-events-none absolute inset-0 z-20 transition-all duration-150",
CARD_RADIUS_CLASS,
isInteractive &&
!isResizing &&
!isDragging && ["ring-border ring-1", "group-hover/grid-item:ring-foreground/15"],
isResizing && "outline-foreground/25 bg-foreground/5 outline outline-dashed",
isDragging && "ring-foreground/25 ring-1"
)}
/>
);
}
function GridDropPlaceholder({ group }: { group: string }) {
const info = useGridPlaceholder(group);
if (!info) return null;
return (
<div
style={info.style}
className={cn(
"pointer-events-none absolute z-0",
CARD_RADIUS_CLASS,
"outline-foreground/25 bg-foreground/5 outline outline-dashed"
)}
/>
);
}
type GridItemProps = {
id: string;
group: string;
isDraggable: boolean;
isResizable: boolean;
showHandleOnCorner: boolean;
children: React.ReactNode;
};
function GridItem({
id,
group,
isDraggable,
isResizable,
showHandleOnCorner,
children,
}: GridItemProps) {
const { ref, style, isDragging, handleRef } = useGridItem({ id, group });
const {
ref: resizeRef,
handleProps,
isResizing,
} = useGridResizeHandle({ id, handle: "se", group });
const itemRef = React.useRef<HTMLDivElement>(null);
const [handleVisible, setHandleVisible] = React.useState(false);
const onMouseMove = React.useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!showHandleOnCorner || !itemRef.current) return;
const rect = itemRef.current.getBoundingClientRect();
setHandleVisible(
e.clientX >= rect.right - CORNER_ZONE && e.clientY >= rect.bottom - CORNER_ZONE
);
},
[showHandleOnCorner]
);
const onMouseLeave = React.useCallback(() => setHandleVisible(false), []);
const setRef = useMergedRef<HTMLDivElement>(itemRef, ref as React.Ref<HTMLDivElement>);
const isHandleVisible = isResizing || (showHandleOnCorner ? handleVisible : true);
return (
<WidgetIdContext.Provider value={id}>
<WidgetDragHandleRefContext.Provider value={handleRef}>
<div
ref={setRef}
style={style}
className="group/grid-item relative"
onMouseMove={showHandleOnCorner ? onMouseMove : undefined}
onMouseLeave={showHandleOnCorner ? onMouseLeave : undefined}
>
<GridItemOverlay
isResizing={isResizing}
isDragging={isDragging}
isInteractive={isDraggable || isResizable}
/>
<div className={cn("relative h-full w-full overflow-hidden", CARD_RADIUS_CLASS)}>
{children}
</div>
{isResizable && (
<div
ref={resizeRef}
{...handleProps}
style={{ width: RESIZE_HANDLE_SIZE, height: RESIZE_HANDLE_SIZE, bottom: 0, right: 0 }}
className="absolute z-30 cursor-nwse-resize touch-none"
>
<svg
width={RESIZE_HANDLE_SIZE}
height={RESIZE_HANDLE_SIZE}
viewBox={`0 0 ${RESIZE_HANDLE_SIZE} ${RESIZE_HANDLE_SIZE}`}
fill="none"
overflow="visible"
aria-hidden
style={{ opacity: isHandleVisible ? 1 : 0, transition: "opacity 150ms" }}
>
<path
d={resizePath}
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={cn(
"transition-colors duration-150",
isResizing ? "text-foreground/50" : "text-foreground/33"
)}
/>
</svg>
</div>
)}
</div>
</WidgetDragHandleRefContext.Provider>
</WidgetIdContext.Provider>
);
}
// ─────────────────────────────────────────────────────────────────────────────
// § 6. <WidgetGrid> — the one public grid component
// ─────────────────────────────────────────────────────────────────────────────
export type GridProps = {
layout: Layout;
onLayoutChange: (layout: Layout) => void;
/** Measured container width hint for SSR. Defaults to 1200. */
initialWidth?: number;
isDraggable?: boolean;
isResizable?: boolean;
/** Only show the SE resize handle near the corner. Default true. */
showHandleOnCorner?: boolean;
className?: string;
children: React.ReactNode;
/**
* Rendered before the grid mounts (SSR / hydration gap).
* Receives the measured container width so you can size it identically.
* Use <WidgetGridSkeleton> here.
*/
skeleton?: (containerWidth: number) => React.ReactNode;
};
export function WidgetGrid({
layout,
onLayoutChange,
initialWidth = 1200,
isDraggable = true,
isResizable = true,
showHandleOnCorner = true,
className,
skeleton,
children,
}: GridProps) {
const { width, mounted, containerRef } = useContainerWidth({ initialWidth });
return (
<div ref={containerRef} className={cn("w-full min-w-0", className)}>
{!mounted && skeleton ? (
skeleton(width)
) : mounted ? (
<DragDropProvider>
<GridInner
layout={layout}
onLayoutChange={onLayoutChange}
width={width}
isDraggable={isDraggable}
isResizable={isResizable}
showHandleOnCorner={showHandleOnCorner}
>
{children}
</GridInner>
</DragDropProvider>
) : null}
</div>
);
}
// GridInner — DragDropProvider must wrap useGridContainer, so it lives here.
// Not exported: it's an implementation detail, not part of the public API.
function GridInner({
layout,
onLayoutChange,
width,
isDraggable,
isResizable,
showHandleOnCorner,
children,
}: Omit<GridProps, "initialWidth" | "className" | "skeleton"> & { width: number }) {
const [dragState, setDragState] = React.useState({
isDragging: false,
justDropped: false,
});
const { containerProps, group } = useGridContainer({
layout,
width,
onLayoutChange,
compactor: verticalCompactor,
isDraggable,
isResizable,
gridConfig: {
cols: GRID_COLS,
rowHeight: GRID_ROW_HEIGHT,
margin: [GRID_MARGIN, GRID_MARGIN],
containerPadding: [0, 0],
},
resizeConfig: { enabled: isResizable, handles: ["se"] },
onDragStart: () => setDragState({ isDragging: true, justDropped: false }),
onDragStop: () => setDragState({ isDragging: false, justDropped: true }),
});
// Clear justDropped after the drop frame has painted.
React.useLayoutEffect(() => {
if (!dragState.justDropped) return;
const raf = requestAnimationFrame(() =>
setDragState({ isDragging: false, justDropped: false })
);
return () => cancelAnimationFrame(raf);
}, [dragState.justDropped]);
const removeWidget = React.useCallback(
(id: string) => {
onLayoutChange(
removeItemWithCompactor(layout, id, { compactor: verticalCompactor, cols: GRID_COLS })
);
},
[layout, onLayoutChange]
);
// Build id→node map so layout order drives render, not JSX order.
const childMap = React.useMemo(() => {
const map = new Map<string, React.ReactNode>();
React.Children.forEach(children, (child) => {
if (React.isValidElement(child) && child.key != null) {
map.set(String(child.key).replace(/^\.\$/, ""), child);
}
});
return map;
}, [children]);
return (
<RemoveWidgetContext.Provider value={removeWidget}>
<GridDragStateContext.Provider value={dragState}>
<div {...containerProps} className="w-full">
<GridDropPlaceholder group={group} />
{layout.map((item: LayoutItem) => (
<GridItem
key={item.i}
id={item.i}
group={group}
isDraggable={isDraggable ?? true}
isResizable={isResizable ?? true}
showHandleOnCorner={showHandleOnCorner ?? true}
>
{childMap.get(item.i)}
</GridItem>
))}
</div>
</GridDragStateContext.Provider>
</RemoveWidgetContext.Provider>
);
}
// ─────────────────────────────────────────────────────────────────────────────
// § 7. Widget shell — composable card primitives
// ─────────────────────────────────────────────────────────────────────────────
// ─── Widget ──────────────────────────────────────────────────────────────────
export type WidgetProps = React.HTMLAttributes<HTMLDivElement>;
export const Widget = React.forwardRef<HTMLDivElement, WidgetProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn("bg-card text-card-foreground flex h-full w-full flex-col", className)}
{...props}
>
{children}
</div>
)
);
Widget.displayName = "Widget";
// ─── WidgetHeader ─────────────────────────────────────────────────────────────
export interface WidgetHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
borderless?: boolean;
}
export const WidgetHeader = React.forwardRef<HTMLDivElement, WidgetHeaderProps>(
({ className, borderless, children, ...props }, forwardedRef) => {
const handleRef = useWidgetDragHandle();
const setRef = useMergedRef<HTMLDivElement>(
forwardedRef as React.Ref<HTMLDivElement>,
handleRef as React.Ref<HTMLDivElement>
);
return (
<div
ref={setRef}
className={cn(
"flex shrink-0 cursor-grab touch-none items-center gap-1.5 px-3 py-2 select-none",
"active:cursor-grabbing",
!borderless && "border-border/60 border-b",
className
)}
{...props}
>
{children}
</div>
);
}
);
WidgetHeader.displayName = "WidgetHeader";
// ─── WidgetGrip ───────────────────────────────────────────────────────────────
export type WidgetGripProps = React.HTMLAttributes<HTMLSpanElement>;
export const WidgetGrip = React.forwardRef<HTMLSpanElement, WidgetGripProps>(
({ className, ...props }, ref) => (
<span
ref={ref}
aria-hidden
className={cn("text-muted-foreground/60 shrink-0 leading-none", className)}
{...props}
>
<GripVertical size={14} />
</span>
)
);
WidgetGrip.displayName = "WidgetGrip";
// ─── WidgetTitle ──────────────────────────────────────────────────────────────
export type WidgetTitleProps = React.HTMLAttributes<HTMLHeadingElement>;
export const WidgetTitle = React.forwardRef<HTMLHeadingElement, WidgetTitleProps>(
({ className, children, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-foreground min-w-0 flex-1 truncate text-sm leading-none font-medium",
className
)}
{...props}
>
{children}
</h3>
)
);
WidgetTitle.displayName = "WidgetTitle";
// ─── WidgetActions ────────────────────────────────────────────────────────────
export interface WidgetActionsProps extends React.HTMLAttributes<HTMLDivElement> {
hideClose?: boolean;
closeButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
}
export const WidgetActions = React.forwardRef<HTMLDivElement, WidgetActionsProps>(
({ className, children, hideClose = false, closeButtonProps, ...props }, ref) => {
const remove = useRemoveWidget();
return (
<div ref={ref} className={cn("flex shrink-0 items-center gap-1", className)} {...props}>
{children}
{!hideClose && remove && (
<button
type="button"
aria-label="Remove widget"
onClick={remove}
{...closeButtonProps}
className={cn(
"-me-1 flex size-5 cursor-pointer items-center justify-center",
"rounded-[calc(var(--radius-sm)-2px)]",
"text-muted-foreground/60 transition-colors",
"hover:bg-muted-foreground/20 hover:text-foreground/80",
"focus-visible:ring-ring focus-visible:ring-1 focus-visible:outline-none",
closeButtonProps?.className
)}
>
<X size={14} strokeWidth={2} />
</button>
)}
</div>
);
}
);
WidgetActions.displayName = "WidgetActions";
// ─── WidgetBody ───────────────────────────────────────────────────────────────
export interface WidgetBodyProps extends React.HTMLAttributes<HTMLDivElement> {
padding?: "default" | "flush";
}
export const WidgetBody = React.forwardRef<HTMLDivElement, WidgetBodyProps>(
({ className, padding = "default", children, ...props }, ref) => (
<div
ref={ref}
className={cn(
"relative min-h-0 flex-1 overflow-auto",
padding === "default" && "p-3",
className
)}
{...props}
>
{children}
</div>
)
);
WidgetBody.displayName = "WidgetBody";
// ─── WidgetSeparator ──────────────────────────────────────────────────────────
export type WidgetSeparatorProps = React.HTMLAttributes<HTMLHRElement>;
export const WidgetSeparator = React.forwardRef<HTMLHRElement, WidgetSeparatorProps>(
({ className, ...props }, ref) => (
<hr ref={ref} className={cn("border-border/60", className)} {...props} />
)
);
WidgetSeparator.displayName = "WidgetSeparator";
// ─────────────────────────────────────────────────────────────────────────────
// § 8. Skeleton
// ─────────────────────────────────────────────────────────────────────────────
//
// Uses CSS grid (not absolute positioning) — same approach as snapgrid's own
// WidgetGridSkeleton. Derives all sizing from GRID_COLS / GRID_ROW_HEIGHT / GRID_MARGIN
// so it matches the live grid pixel-for-pixel.
// ─── WidgetGridSkeletonPulse ─────────────────────────────────────────────────
export interface WidgetGridSkeletonPulseProps extends React.HTMLAttributes<HTMLDivElement> {
label?: string;
}
export const WidgetGridSkeletonPulse = React.forwardRef<
HTMLDivElement,
WidgetGridSkeletonPulseProps
>(({ className, label = "Loading data...", ...props }, ref) => (
<div ref={ref} className={cn("relative h-full w-full", className)} {...props}>
<div className="bg-muted absolute inset-0 animate-pulse" />
<div className="absolute inset-0 flex items-center justify-center">
<span className="border-border bg-background text-foreground flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs">
<LoaderCircle className="size-3 animate-spin" />
{label}
</span>
</div>
</div>
));
WidgetGridSkeletonPulse.displayName = "WidgetGridSkeletonPulse";
// ─── WidgetGridSkeletonCard ─────────────────────────────────────────────────────────
export interface WidgetGridSkeletonCardProps extends React.HTMLAttributes<HTMLDivElement> {
header?: React.ReactNode;
body?: React.ReactNode;
}
export const WidgetGridSkeletonCard = React.forwardRef<HTMLDivElement, WidgetGridSkeletonCardProps>(
({ className, header, body, ...props }, ref) => (
<div
ref={ref}
className={cn("bg-card ring-border relative overflow-hidden rounded-lg ring-1", className)}
{...props}
>
<div
className={cn(
"border-border bg-foreground/5 flex h-7 w-full shrink-0",
"items-center gap-1.5 border-b px-2 text-xs"
)}
>
<GripVertical className="text-muted-foreground/40 size-3.5 shrink-0" />
<span className="text-foreground/60 flex-1 truncate">{header}</span>
<button
disabled
aria-label="Close"
className="text-muted-foreground/60 -me-1 flex size-5 shrink-0 items-center justify-center rounded disabled:pointer-events-none disabled:opacity-30"
>
<X size={14} />
</button>
</div>
<div className="absolute inset-0 top-7">{body ?? <WidgetGridSkeletonPulse />}</div>
</div>
)
);
WidgetGridSkeletonCard.displayName = "WidgetGridSkeletonCard";
// ─── WidgetGridSkeleton ───────────────────────────────────────────────────────
export interface WidgetGridSkeletonSlot {
header?: React.ReactNode;
body?: React.ReactNode;
}
export interface WidgetGridSkeletonProps {
layout: Layout;
/**
* Render-prop for per-item customisation: receives the LayoutItem,
* returns { header?, body? }.
*/
children?: (item: LayoutItem) => WidgetGridSkeletonSlot;
/**
* Shorthand title map { [itemId]: title }.
* Ignored when children is provided.
*/
titles?: Record<string, React.ReactNode>;
className?: string;
}
export function WidgetGridSkeleton({
layout,
children,
titles,
className,
}: WidgetGridSkeletonProps) {
return (
<div
aria-hidden
className={cn("w-full", className)}
style={{
display: "grid",
gridTemplateColumns: `repeat(${GRID_COLS}, minmax(0, 1fr))`,
gridAutoRows: `${GRID_ROW_HEIGHT}px`,
gap: GRID_MARGIN,
}}
>
{layout.map((item) => {
const slot = children ? children(item) : { header: titles?.[item.i] ?? item.i };
return (
<WidgetGridSkeletonCard
key={item.i}
header={slot.header}
body={slot.body}
style={{
gridColumn: `${item.x + 1} / span ${item.w}`,
gridRow: `${item.y + 1} / span ${item.h}`,
}}
/>
);
})}
</div>
);
} |
All reactions
|
thanks for the suggestion, weird how it has only 8 stars |
All reactions
|
The dev made the package this year. I went package hunting and that's how I discovered it. |
All reactions
-
🚀 1
|
did that yesterday and happily got to your comment :) |
All reactions
-
❤️ 1
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Overview
I'd like to propose a new official registry block: rgl-dashboard - a draggable, resizable multi-dashboard built on
react-grid-layout, styled entirely with shadcn/ui components and Tailwind.What It Does
Layout engine: Thin wrapper around react-grid-layout with shadcn-compatible Tailwind styling for placeholders and resize handles
Widget system: 6 built-in tile types (intro, kpi, chart, bar, table, metric) using Recharts + OKLCH-safe CSS variables
Interactive demo: Full CRUD — add/remove tiles, rename dashboards, switch between multiple dashboards, edit mode toggle
Persistence: Opt-in via storageKey prop (default is in-memory only)
Dark mode: All charts use var(--primary), var(--border), etc. — no hsl(var(--...)) wrappers
Files Added
ui/rgl-dashboard-grid.tsx — grid wrapper
ui/rgl-dashboard-tile.tsx — tile renderer
lib/rgl-dashboard-types.ts — types + layout helpers
lib/rgl-dashboard-storage.ts — load/save utilities
lib/rgl-dashboard-example.ts — seed data constant
blocks/rgl-dashboard/page.tsx + rgl-dashboard-demo.tsx — full block
examples/* — 6 demo variants for docs
content/docs/components/radix/rgl-dashboard-{grid,tile}.mdx — docs
Why This Belongs in the Official Registry
Common pattern: Dashboard grids are one of the most requested layouts in Discord/GitHub discussions, but there's no official opinionated solution today
Composable: The grid wrapper and tile renderer are independent registry:ui items, not just block internals
Zero data dependencies: Pure presentation layer; consumers bring their own data
Maintained dependency: react-grid-layout is mature (1.5.x) and widely used; wrapping it correctly with shadcn styling is non-trivial to do ad-hoc
Open Questions for Maintainers
react-grid-layout version: Currently pinned to 1.5.3 in registry dependencies. The v4 app uses ^1.4.4. Should the registry declare a caret range (^1.5.0) or stay exact?
Recharts: Block uses recharts@3.8.0 which matches the v4 app. Any concerns about registry items declaring this version explicitly?
Scope: Should rgl-dashboard-grid and rgl-dashboard-tile remain independent registry:ui items, or be block-only internals?
PR
#10467 (comment)
Preview
Images
All reactions