Add responsive grid layout for mobile/tablet support
Switch from fixed 48-column grid to responsive breakpoints: - lg (>=1200px): 48 columns, free positioning (unchanged desktop behavior) - md (>=900px): 24 columns, vertical compaction - sm (>=600px): 12 columns, vertical compaction - xs (<600px): 6 columns, vertical compaction Widgets automatically reflow and stack on smaller screens instead of being squished. Layout changes are only persisted from the desktop breakpoint. Drag/resize editing is desktop-only. Also adds mobile CSS refinements for topbar, tabs, and workspace padding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
Generated
+1783
File diff suppressed because it is too large
Load Diff
@@ -1255,6 +1255,62 @@ button:disabled {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 230px;
|
width: 230px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.adminCardGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.topBar {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brandLogoFrame {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileButton {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboardWorkspace {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topEditModeButton {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topBar .topEditModeButton {
|
||||||
|
right: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboardTabsBar {
|
||||||
|
padding: 4px 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editToolbar {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editToolbar p {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.singleNoteWidget {
|
.singleNoteWidget {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ComponentType, ReactNode } from "react";
|
import type { ComponentType, ReactNode } from "react";
|
||||||
import { useMemo } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import ReactGridLayoutBase, { WidthProvider } from "react-grid-layout/legacy";
|
import { Responsive, WidthProvider } from "react-grid-layout/legacy";
|
||||||
import type { DashboardGridWidget, DashboardLayoutItem } from "@/lib/dashboard-layout";
|
import type { DashboardGridWidget, DashboardLayoutItem } from "@/lib/dashboard-layout";
|
||||||
|
|
||||||
export type DashboardGridProps = {
|
export type DashboardGridProps = {
|
||||||
@@ -13,7 +13,10 @@ export type DashboardGridProps = {
|
|||||||
onLayoutChange: (layout: DashboardLayoutItem[]) => void;
|
onLayoutChange: (layout: DashboardLayoutItem[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const WidthAwareGridLayout = WidthProvider(ReactGridLayoutBase) as ComponentType<any>;
|
const ResponsiveGridLayout = WidthProvider(Responsive) as ComponentType<any>;
|
||||||
|
|
||||||
|
const BREAKPOINTS = { lg: 1200, md: 900, sm: 600, xs: 0 };
|
||||||
|
const COLS = { lg: 48, md: 24, sm: 12, xs: 6 };
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number): number {
|
function clamp(value: number, min: number, max: number): number {
|
||||||
return Math.max(min, Math.min(max, value));
|
return Math.max(min, Math.min(max, value));
|
||||||
@@ -21,44 +24,26 @@ function clamp(value: number, min: number, max: number): number {
|
|||||||
|
|
||||||
function getWidgetMinimumSize(widget: DashboardGridWidget): { minW: number; minH: number } {
|
function getWidgetMinimumSize(widget: DashboardGridWidget): { minW: number; minH: number } {
|
||||||
if (widget.type === "search") {
|
if (widget.type === "search") {
|
||||||
return {
|
return { minW: 8, minH: 5 };
|
||||||
minW: 8,
|
|
||||||
minH: 5
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.type === "clock") {
|
if (widget.type === "clock") {
|
||||||
return {
|
return { minW: 4, minH: 4 };
|
||||||
minW: 4,
|
|
||||||
minH: 4
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.type === "calendar") {
|
if (widget.type === "calendar") {
|
||||||
return {
|
return { minW: 8, minH: 6 };
|
||||||
minW: 8,
|
|
||||||
minH: 6
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.type === "calculator") {
|
if (widget.type === "calculator") {
|
||||||
return {
|
return { minW: 8, minH: 6 };
|
||||||
minW: 8,
|
|
||||||
minH: 6
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.type === "note") {
|
if (widget.type === "note") {
|
||||||
return {
|
return { minW: 4, minH: 5 };
|
||||||
minW: 4,
|
|
||||||
minH: 5
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { minW: 4, minH: 4 };
|
||||||
minW: 4,
|
|
||||||
minH: 4
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function widgetsToLayout(widgets: DashboardGridWidget[]): DashboardLayoutItem[] {
|
function widgetsToLayout(widgets: DashboardGridWidget[]): DashboardLayoutItem[] {
|
||||||
@@ -104,7 +89,25 @@ export default function DashboardGrid({
|
|||||||
renderWidget,
|
renderWidget,
|
||||||
onLayoutChange
|
onLayoutChange
|
||||||
}: DashboardGridProps) {
|
}: DashboardGridProps) {
|
||||||
const layout = useMemo(() => widgetsToLayout(widgets), [widgets]);
|
const [currentBreakpoint, setCurrentBreakpoint] = useState("lg");
|
||||||
|
const breakpointRef = useRef("lg");
|
||||||
|
|
||||||
|
const isDesktop = currentBreakpoint === "lg";
|
||||||
|
|
||||||
|
const layouts = useMemo(() => ({
|
||||||
|
lg: widgetsToLayout(widgets)
|
||||||
|
}), [widgets]);
|
||||||
|
|
||||||
|
const handleBreakpointChange = useCallback((breakpoint: string) => {
|
||||||
|
breakpointRef.current = breakpoint;
|
||||||
|
setCurrentBreakpoint(breakpoint);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLayoutChange = useCallback((layout: DashboardLayoutItem[]) => {
|
||||||
|
if (breakpointRef.current === "lg") {
|
||||||
|
onLayoutChange(normalizeLayout(layout));
|
||||||
|
}
|
||||||
|
}, [onLayoutChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="widgetGridShell">
|
<div className="widgetGridShell">
|
||||||
@@ -116,24 +119,26 @@ export default function DashboardGrid({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{widgets.length > 0 ? (
|
{widgets.length > 0 ? (
|
||||||
<WidthAwareGridLayout
|
<ResponsiveGridLayout
|
||||||
className="widgetGrid"
|
className="widgetGrid"
|
||||||
layout={layout}
|
layouts={layouts}
|
||||||
cols={48}
|
breakpoints={BREAKPOINTS}
|
||||||
|
cols={COLS}
|
||||||
rowHeight={8}
|
rowHeight={8}
|
||||||
margin={[12, 12]}
|
margin={[12, 12]}
|
||||||
containerPadding={[0, 0]}
|
containerPadding={[0, 0]}
|
||||||
compactType={null}
|
compactType={isDesktop ? null : "vertical"}
|
||||||
preventCollision={true}
|
preventCollision={isDesktop}
|
||||||
isBounded={false}
|
isBounded={false}
|
||||||
autoSize={true}
|
autoSize={true}
|
||||||
isDraggable={editMode}
|
isDraggable={editMode && isDesktop}
|
||||||
isResizable={editMode}
|
isResizable={editMode && isDesktop}
|
||||||
draggableHandle=".widgetDragHandle"
|
draggableHandle=".widgetDragHandle"
|
||||||
draggableCancel=".widgetNoDrag"
|
draggableCancel=".widgetNoDrag"
|
||||||
resizeHandles={["se"]}
|
resizeHandles={["se"]}
|
||||||
measureBeforeMount={true}
|
measureBeforeMount={true}
|
||||||
onLayoutChange={(nextLayout: DashboardLayoutItem[]) => onLayoutChange(normalizeLayout(nextLayout))}
|
onBreakpointChange={handleBreakpointChange}
|
||||||
|
onLayoutChange={handleLayoutChange}
|
||||||
>
|
>
|
||||||
{widgets.map((widget) => (
|
{widgets.map((widget) => (
|
||||||
<div
|
<div
|
||||||
@@ -143,7 +148,7 @@ export default function DashboardGrid({
|
|||||||
{renderWidget(widget)}
|
{renderWidget(widget)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</WidthAwareGridLayout>
|
</ResponsiveGridLayout>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-2
@@ -15,7 +15,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|||||||
Reference in New Issue
Block a user