diff --git a/data/configs/default.json b/data/configs/default.json index 9c48b9103..a14211f4e 100644 --- a/data/configs/default.json +++ b/data/configs/default.json @@ -8,90 +8,33 @@ "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f", "position": 0, "name": "Example Category" + }, + { + "id": "c8407d2c-2353-4775-87c3-602f6f2684d5", + "name": "Test", + "position": 4 + }, + { + "id": "c1c4bec3-1044-4a80-957f-afe7ff49f421", + "name": "Test", + "position": 2 } ], "wrappers": [ { - "id": "default", + "id": "5823c4d6-6baf-4436-b990-93fe77e1dc62", "position": 1 + }, + { + "id": "943f0681-a15b-4576-9a61-a74bd6fdd3ab", + "position": 3 + }, + { + "id": "default", + "position": 5 } ], "apps": [ - { - "id": "e41a11f5-9c6e-41bc-ac0e-4c4c47582faa", - "name": "Your app", - "url": "https://homarr.dev", - "appearance": { - "iconUrl": "/imgs/logo/logo.png" - }, - "network": { - "enabledStatusChecker": false, - "okStatus": [] - }, - "behaviour": { - "isOpeningNewTab": true, - "externalUrl": "" - }, - "area": { - "type": "wrapper", - "properties": { - "id": "default" - } - }, - "shape": { - "location": { - "x": 8, - "y": 12 - }, - "size": { - "width": 4, - "height": 2 - } - }, - "integration": { - "type": null, - "properties": [] - } - }, - { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330", - "name": "Contribute", - "url": "https://github.com/ajnart/homarr", - "behaviour": { - "onClickUrl": "https://github.com/ajnart/homarr", - "externalUrl": "https://github.com/ajnart/homarr", - "isOpeningNewTab": true - }, - "network": { - "enabledStatusChecker": false, - "okStatus": [ - 200 - ] - }, - "appearance": { - "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png" - }, - "integration": { - "type": null, - "properties": [] - }, - "area": { - "type": "category", - "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" - } - }, - "shape": { - "location": { - "x": 4, - "y": 2 - }, - "size": { - "width": 2, - "height": 2 - } - } - }, { "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337", "name": "Discord", @@ -121,19 +64,41 @@ } }, "shape": { - "location": { - "x": 0, - "y": 4 + "md": { + "location": { + "x": 2, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } }, - "size": { - "width": 4, - "height": 3 + "sm": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } } } }, { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33a", - "name": "Documentation", + "id": "5df743d9-5cb1-457c-85d2-64ff86855652", + "name": "Your app", "url": "https://homarr.dev", "behaviour": { "onClickUrl": "https://homarr.dev", @@ -154,19 +119,41 @@ "properties": [] }, "area": { - "type": "wrapper", + "type": "category", "properties": { - "id": "default" + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" } }, "shape": { - "location": { - "x": 8, - "y": 9 + "md": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 2, + "height": 1 + } }, - "size": { - "width": 4, - "height": 3 + "sm": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 2, + "height": 1 + } } } }, @@ -186,7 +173,7 @@ ] }, "appearance": { - "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png" + "iconUrl": "https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/61e1116779fc0a9bd5bdbcc7_Frame%206.png" }, "integration": { "type": null, @@ -199,13 +186,152 @@ } }, "shape": { - "location": { - "x": 2, - "y": 2 + "md": { + "location": { + "x": 3, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } }, - "size": { - "width": 2, - "height": 2 + "sm": { + "location": { + "x": 3, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 3, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + } + } + }, + { + "id": "e41a11f5-9c6e-41bc-ac0e-4c4c47582faa", + "name": "Your app", + "url": "https://homarr.dev", + "appearance": { + "iconUrl": "/imgs/logo/logo.png" + }, + "network": { + "enabledStatusChecker": false, + "okStatus": [] + }, + "behaviour": { + "isOpeningNewTab": true, + "externalUrl": "" + }, + "area": { + "type": "sidebar", + "properties": { + "location": "left" + } + }, + "shape": { + "md": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + } + }, + "integration": { + "type": null, + "properties": [] + } + }, + { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330", + "name": "Contribute", + "url": "https://github.com/ajnart/homarr", + "behaviour": { + "onClickUrl": "https://github.com/ajnart/homarr", + "externalUrl": "https://github.com/ajnart/homarr", + "isOpeningNewTab": true + }, + "network": { + "enabledStatusChecker": false, + "okStatus": [] + }, + "appearance": { + "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png" + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 0, + "y": 2 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "sm": { + "location": { + "x": 0, + "y": 2 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 2 + }, + "size": { + "width": 1, + "height": 1 + } } } }, @@ -227,17 +353,39 @@ "area": { "type": "category", "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + "id": "c8407d2c-2353-4775-87c3-602f6f2684d5" } }, "shape": { - "location": { - "x": 0, - "y": 2 + "md": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 6, + "height": 1 + } }, - "size": { - "width": 2, - "height": 2 + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 8, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 8, + "height": 1 + } } }, "integration": { @@ -246,7 +394,7 @@ } }, { - "id": "5df743d9-5cb1-457c-85d2-64ff86855652", + "id": "615e43bd-f0aa-4117-ba49-b6495c039f3e", "name": "Your app", "url": "https://homarr.dev", "appearance": { @@ -267,13 +415,35 @@ } }, "shape": { - "location": { - "x": 0, - "y": 7 + "sm": { + "location": { + "x": 0, + "y": 5 + }, + "size": { + "width": 1, + "height": 1 + } }, - "size": { - "width": 6, - "height": 4 + "md": { + "location": { + "x": 0, + "y": 5 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 5 + }, + "size": { + "width": 1, + "height": 1 + } } }, "integration": { @@ -283,51 +453,6 @@ } ], "widgets": [ - { - "id": "date", - "properties": { - "display24HourFormat": true - }, - "area": { - "type": "category", - "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" - } - }, - "shape": { - "location": { - "x": 0, - "y": 0 - }, - "size": { - "width": 6, - "height": 2 - } - } - }, - { - "id": "weather", - "properties": { - "displayInFahrenheit": false, - "location": "Paris" - }, - "area": { - "type": "wrapper", - "properties": { - "id": "default" - } - }, - "shape": { - "location": { - "x": 0, - "y": 0 - }, - "size": { - "width": 4, - "height": 2 - } - } - }, { "id": "calendar", "properties": { @@ -340,13 +465,124 @@ } }, "shape": { - "location": { - "x": 0, - "y": 2 + "md": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 12, + "height": 5 + } }, - "size": { - "width": 5, - "height": 5 + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 12, + "height": 5 + } + }, + "lg": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 12, + "height": 5 + } + } + } + }, + { + "id": "weather", + "properties": { + "displayInFahrenheit": false, + "location": "Paris" + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + } + } + }, + { + "id": "date", + "properties": { + "display24HourFormat": true + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "sm": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "md": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "lg": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } } } } @@ -360,8 +596,8 @@ }, "customization": { "layout": { - "enabledLeftSidebar": false, - "enabledRightSidebar": false, + "enabledLeftSidebar": true, + "enabledRightSidebar": true, "enabledDocker": false, "enabledPing": false, "enabledSearchbar": true @@ -372,9 +608,9 @@ "backgroundImageUrl": "", "customCss": "", "colors": { - "primary": "red", - "secondary": "orange", - "shade": 5 + "primary": "pink", + "secondary": "yellow", + "shade": 4 }, "appOpacity": 100 } diff --git a/public/locales/en/layout/modals/change-position.json b/public/locales/en/layout/modals/change-position.json index 49f39e563..464a676de 100644 --- a/public/locales/en/layout/modals/change-position.json +++ b/public/locales/en/layout/modals/change-position.json @@ -4,5 +4,5 @@ "height": "Height", "yPosition": "Y axis position", "zeroOrHigher": "0 or higher", - "betweenXandY": "Between {{mim}} and {{max}}" + "betweenXandY": "Between {{min}} and {{max}}" } \ No newline at end of file diff --git a/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx b/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx index 8a60c345d..071af487d 100644 --- a/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx +++ b/src/components/Dashboard/Mobile/Ribbon/MobileRibbonSidebarDrawer.tsx @@ -29,7 +29,7 @@ export const MobileRibbonSidebarDrawer = ({ }} {...props} > - + ); }; diff --git a/src/components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal.tsx b/src/components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal.tsx index 820223405..dc18afe0e 100644 --- a/src/components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal.tsx +++ b/src/components/Dashboard/Modals/ChangePosition/ChangeAppPositionModal.tsx @@ -3,6 +3,7 @@ import { closeModal, ContextModalProps } from '@mantine/modals'; import { useConfigContext } from '../../../../config/provider'; import { useConfigStore } from '../../../../config/store'; import { AppType } from '../../../../types/app'; +import { useGridstackStore, useWrapperColumnCount } from '../../Wrappers/gridstack/store'; import { ChangePositionModal } from './ChangePositionModal'; type ChangeAppPositionModalInnerProps = { @@ -16,6 +17,9 @@ export const ChangeAppPositionModal = ({ }: ContextModalProps) => { const { name: configName } = useConfigContext(); const updateConfig = useConfigStore((x) => x.updateConfig); + const shapeSize = useGridstackStore((x) => x.currentShapeSize); + + if (!shapeSize) return null; const handleSubmit = (x: number, y: number, width: number, height: number) => { if (!configName) { @@ -28,7 +32,13 @@ export const ChangeAppPositionModal = ({ ...previousConfig, apps: [ ...previousConfig.apps.filter((x) => x.id !== innerProps.app.id), - { ...innerProps.app, shape: { location: { x, y }, size: { width, height } } }, + { + ...innerProps.app, + shape: { + ...innerProps.app.shape, + [shapeSize]: { location: { x, y }, size: { width, height } }, + }, + }, ], }), true @@ -49,28 +59,35 @@ export const ChangeAppPositionModal = ({ onCancel={handleCancel} widthData={widthData} heightData={heightData} - initialX={innerProps.app.shape.location.x} - initialY={innerProps.app.shape.location.y} - initialWidth={innerProps.app.shape.size.width} - initialHeight={innerProps.app.shape.size.height} + initialX={innerProps.app.shape[shapeSize]?.location.x} + initialY={innerProps.app.shape[shapeSize]?.location.y} + initialWidth={innerProps.app.shape[shapeSize]?.size.width} + initialHeight={innerProps.app.shape[shapeSize]?.size.height} /> ); }; -const useHeightData = (): SelectItem[] => - Array.from(Array(11).keys()).map((n) => { - const index = n + 1; - return { - value: index.toString(), - label: `${64 * index}px`, - }; - }); +const useHeightData = (): SelectItem[] => { + const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth); + const wrapperColumnCount = useWrapperColumnCount(); -const useWidthData = (): SelectItem[] => - Array.from(Array(11).keys()).map((n) => { + return Array.from(Array(11).keys()).map((n) => { const index = n + 1; return { value: index.toString(), - label: `${64 * index}px`, + label: `${Math.floor(index * (mainAreaWidth! / wrapperColumnCount!))}px`, }; }); +}; + +const useWidthData = (): SelectItem[] => { + const wrapperColumnCount = useWrapperColumnCount(); + return Array.from(Array(wrapperColumnCount!).keys()).map((n) => { + const index = n + 1; + return { + value: index.toString(), + // eslint-disable-next-line no-mixed-operators + label: `${((100 / wrapperColumnCount!) * index).toFixed(2)}%`, + }; + }); +}; diff --git a/src/components/Dashboard/Modals/ChangePosition/ChangePositionModal.tsx b/src/components/Dashboard/Modals/ChangePosition/ChangePositionModal.tsx index 3dfeb55d0..f8e704b51 100644 --- a/src/components/Dashboard/Modals/ChangePosition/ChangePositionModal.tsx +++ b/src/components/Dashboard/Modals/ChangePosition/ChangePositionModal.tsx @@ -4,10 +4,10 @@ import { useTranslation } from 'next-i18next'; import { useConfigContext } from '../../../../config/provider'; interface ChangePositionModalProps { - initialX: number; - initialY: number; - initialWidth: number; - initialHeight: number; + initialX?: number; + initialY?: number; + initialWidth?: number; + initialHeight?: number; widthData: SelectItem[]; heightData: SelectItem[]; onSubmit: (x: number, y: number, width: number, height: number) => void; @@ -28,10 +28,10 @@ export const ChangePositionModal = ({ const form = useForm({ initialValues: { - x: initialX, - y: initialY, - width: initialWidth, - height: initialHeight, + x: initialX ?? null, + y: initialY ?? null, + width: initialWidth?.toString() ?? '', + height: initialHeight?.toString() ?? '', }, validateInputOnChange: true, validateInputOnBlur: true, @@ -42,7 +42,12 @@ export const ChangePositionModal = ({ return; } - onSubmit(form.values.x, form.values.y, form.values.width, form.values.height); + const width = parseInt(form.values.width, 10); + const height = parseInt(form.values.height, 10); + + if (!form.values.x || !form.values.y || Number.isNaN(width) || Number.isNaN(height)) return; + + onSubmit(form.values.x, form.values.y, width, height); }; const { t } = useTranslation(['layout/modals/change-position', 'common']); @@ -112,8 +117,8 @@ export const ChangePositionModal = ({ }; type FormType = { - x: number; - y: number; - width: number; - height: number; + x: number | null; + y: number | null; + width: string; + height: string; }; diff --git a/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx b/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx index 6e1d92f95..60bfdb9b3 100644 --- a/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx +++ b/src/components/Dashboard/Modals/ChangePosition/ChangeWidgetPositionModal.tsx @@ -4,6 +4,7 @@ import { useConfigContext } from '../../../../config/provider'; import { useConfigStore } from '../../../../config/store'; import widgets from '../../../../widgets'; import { WidgetChangePositionModalInnerProps } from '../../Tiles/Widgets/WidgetsMenu'; +import { useGridstackStore, useWrapperColumnCount } from '../../Wrappers/gridstack/store'; import { ChangePositionModal } from './ChangePositionModal'; export const ChangeWidgetPositionModal = ({ @@ -13,6 +14,7 @@ export const ChangeWidgetPositionModal = ({ }: ContextModalProps) => { const { name: configName } = useConfigContext(); const updateConfig = useConfigStore((x) => x.updateConfig); + const shapeSize = useGridstackStore((x) => x.currentShapeSize); const handleSubmit = (x: number, y: number, width: number, height: number) => { if (!configName) { @@ -23,7 +25,7 @@ export const ChangeWidgetPositionModal = ({ configName, (prev) => { const currentWidget = prev.widgets.find((x) => x.id === innerProps.widgetId); - currentWidget!.shape = { + currentWidget!.shape[shapeSize] = { location: { x, y, @@ -57,32 +59,40 @@ export const ChangeWidgetPositionModal = ({ onCancel={handleCancel} heightData={heightData} widthData={widthData} - initialX={innerProps.widget.shape.location.x} - initialY={innerProps.widget.shape.location.y} - initialWidth={innerProps.widget.shape.size.width} - initialHeight={innerProps.widget.shape.size.height} + initialX={innerProps.widget.shape[shapeSize].location.x} + initialY={innerProps.widget.shape[shapeSize].location.y} + initialWidth={innerProps.widget.shape[shapeSize].size.width} + initialHeight={innerProps.widget.shape[shapeSize].size.height} /> ); }; const useWidthData = (integration: string): SelectItem[] => { + const wrapperColumnCount = useWrapperColumnCount(); const currentWidget = widgets[integration as keyof typeof widgets]; if (!currentWidget) return []; const offset = currentWidget.gridstack.minWidth ?? 2; - const length = (currentWidget.gridstack.maxWidth ?? 12) - offset; - return Array.from({ length }, (_, i) => i + offset).map((n) => ({ + const length = + (currentWidget.gridstack.maxWidth > wrapperColumnCount! + ? wrapperColumnCount! + : currentWidget.gridstack.maxWidth) - offset; + return Array.from({ length: length + 1 }, (_, i) => i + offset).map((n) => ({ value: n.toString(), - label: `${64 * n}px`, + // eslint-disable-next-line no-mixed-operators + label: `${((100 / wrapperColumnCount!) * n).toFixed(2)}%`, })); }; const useHeightData = (integration: string): SelectItem[] => { + const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth); + const wrapperColumnCount = useWrapperColumnCount(); + const currentWidget = widgets[integration as keyof typeof widgets]; if (!currentWidget) return []; const offset = currentWidget.gridstack.minHeight ?? 2; const length = (currentWidget.gridstack.maxHeight ?? 12) - offset; return Array.from({ length }, (_, i) => i + offset).map((n) => ({ value: n.toString(), - label: `${64 * n}px`, + label: `${(mainAreaWidth! / wrapperColumnCount!) * n}px`, })); }; diff --git a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx index febf8ed0d..be42293aa 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx @@ -112,13 +112,35 @@ export const AvailableElementTypes = ({ }, }, shape: { - location: { - x: 0, - y: 0, + sm: { + location: { + x: 0, + y: 0, + }, + size: { + width: appTileDefinition.minWidth, + height: appTileDefinition.minHeight, + }, }, - size: { - width: appTileDefinition.minWidth, - height: appTileDefinition.minHeight, + md: { + location: { + x: 0, + y: 0, + }, + size: { + width: appTileDefinition.minWidth, + height: appTileDefinition.minHeight, + }, + }, + lg: { + location: { + x: 0, + y: 0, + }, + size: { + width: appTileDefinition.minWidth, + height: appTileDefinition.minHeight, + }, }, }, integration: { diff --git a/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx b/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx index af0e61902..ce3a2b708 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/WidgetElementType.tsx @@ -47,13 +47,35 @@ export const WidgetElementType = ({ id, image, disabled, widget }: WidgetElement }, }, shape: { - location: { - x: 0, - y: 0, + sm: { + location: { + x: 0, + y: 0, + }, + size: { + width: widget.gridstack.minWidth, + height: widget.gridstack.minHeight, + }, }, - size: { - width: widget.gridstack.minWidth, - height: widget.gridstack.minHeight, + md: { + location: { + x: 0, + y: 0, + }, + size: { + width: widget.gridstack.minWidth, + height: widget.gridstack.minHeight, + }, + }, + lg: { + location: { + x: 0, + y: 0, + }, + size: { + width: widget.gridstack.minWidth, + height: widget.gridstack.minHeight, + }, }, }, }, diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx index bc0a7c11e..7a380794c 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx @@ -2,6 +2,7 @@ import { Title } from '@mantine/core'; import { useTranslation } from 'next-i18next'; import { openContextModalGeneric } from '../../../../tools/mantineModalManagerExtensions'; import { IWidget } from '../../../../widgets/widgets'; +import { useWrapperColumnCount } from '../../Wrappers/gridstack/store'; import { GenericTileMenu } from '../GenericTileMenu'; import { WidgetEditModalInnerProps } from './WidgetsEditModal'; import { WidgetsRemoveModalInnerProps } from './WidgetsRemoveModal'; @@ -10,6 +11,7 @@ import WidgetsDefinitions from '../../../../widgets'; export type WidgetChangePositionModalInnerProps = { widgetId: string; widget: IWidget; + wrapperColumnCount: number; }; interface WidgetsMenuProps { @@ -19,8 +21,9 @@ interface WidgetsMenuProps { export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => { const { t } = useTranslation(`modules/${integration}`); + const wrapperColumnCount = useWrapperColumnCount(); - if (!widget) return null; + if (!widget || !wrapperColumnCount) return null; // Match widget.id with WidgetsDefinitions // First get the keys const keys = Object.keys(WidgetsDefinitions); @@ -48,6 +51,7 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => { innerProps: { widgetId: integration, widget, + wrapperColumnCount, }, }); }; diff --git a/src/components/Dashboard/Views/DashboardView.tsx b/src/components/Dashboard/Views/DashboardView.tsx index cacb40263..11f6cb0b4 100644 --- a/src/components/Dashboard/Views/DashboardView.tsx +++ b/src/components/Dashboard/Views/DashboardView.tsx @@ -1,39 +1,83 @@ -import { Group, Stack } from '@mantine/core'; -import { useMemo } from 'react'; +import { Center, Group, Loader, Stack } from '@mantine/core'; +import { useEffect, useMemo, useRef } from 'react'; import { useConfigContext } from '../../../config/provider'; -import { useScreenSmallerThan } from '../../../hooks/useScreenSmallerThan'; +import { useResize } from '../../../hooks/use-resize'; +import { useScreenLargerThan } from '../../../hooks/useScreenLargerThan'; import { CategoryType } from '../../../types/category'; import { WrapperType } from '../../../types/wrapper'; import { DashboardCategory } from '../Wrappers/Category/Category'; +import { useGridstackStore } from '../Wrappers/gridstack/store'; import { DashboardSidebar } from '../Wrappers/Sidebar/Sidebar'; import { DashboardWrapper } from '../Wrappers/Wrapper/Wrapper'; export const DashboardView = () => { const wrappers = useWrapperItems(); - const layoutSettings = useConfigContext()?.config?.settings.customization.layout; - const doNotShowSidebar = useScreenSmallerThan('md'); + const sidebarsVisible = useSidebarVisibility(); + const { isReady, mainAreaRef } = usePrepareGridstack(); return ( - {layoutSettings?.enabledLeftSidebar && !doNotShowSidebar ? ( - - ) : null} - - {wrappers.map((item) => - item.type === 'category' ? ( - - ) : ( - - ) - )} - - {layoutSettings?.enabledRightSidebar && !doNotShowSidebar ? ( - - ) : null} + {sidebarsVisible.isLoading ? ( +
+ +
+ ) : ( + <> + {sidebarsVisible.left ? ( + + ) : null} + + + {!isReady + ? null + : wrappers.map((item) => + item.type === 'category' ? ( + + ) : ( + + ) + )} + + + {sidebarsVisible.right ? ( + + ) : null} + + )}
); }; +const usePrepareGridstack = () => { + const mainAreaRef = useRef(null); + const { width } = useResize(mainAreaRef, []); + const setMainAreaWidth = useGridstackStore((x) => x.setMainAreaWidth); + const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth); + + useEffect(() => { + if (width === 0) return; + setMainAreaWidth(width); + }, [width]); + + return { + isReady: !!mainAreaWidth, + mainAreaRef, + }; +}; + +const useSidebarVisibility = () => { + const layoutSettings = useConfigContext()?.config?.settings.customization.layout; + const screenLargerThanMd = useScreenLargerThan('md'); // For smaller screens mobile ribbons are displayed with drawers + + const isScreenSizeUnknown = typeof screenLargerThanMd === 'undefined'; + + return { + right: layoutSettings?.enabledRightSidebar && screenLargerThanMd, + left: layoutSettings?.enabledLeftSidebar && screenLargerThanMd, + isLoading: isScreenSizeUnknown, + }; +}; + const useWrapperItems = () => { const { config } = useConfigContext(); diff --git a/src/components/Dashboard/Wrappers/Category/Category.tsx b/src/components/Dashboard/Wrappers/Category/Category.tsx index 89da45c00..9034a2e1a 100644 --- a/src/components/Dashboard/Wrappers/Category/Category.tsx +++ b/src/components/Dashboard/Wrappers/Category/Category.tsx @@ -22,7 +22,6 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
diff --git a/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx b/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx index 501ab664e..a8bd89ebe 100644 --- a/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx +++ b/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx @@ -4,11 +4,30 @@ import { useCardStyles } from '../../../layout/useCardStyles'; import { useGridstack } from '../gridstack/use-gridstack'; import { WrapperContent } from '../WrapperContent'; -interface DashboardSidebarProps { +interface DashboardSidebarProps extends DashboardSidebarInnerProps { + location: 'right' | 'left'; + isGridstackReady: boolean; +} + +export const DashboardSidebar = ({ location, isGridstackReady }: DashboardSidebarProps) => ( + + {isGridstackReady && } + +); + +interface DashboardSidebarInnerProps { location: 'right' | 'left'; } -export const DashboardSidebar = ({ location }: DashboardSidebarProps) => { +// Is Required because of the gridstack main area width. +const SidebarInner = ({ location }: DashboardSidebarInnerProps) => { const { refs, apps, widgets } = useGridstack('sidebar', location); const minRow = useMinRowForFullHeight(refs.wrapper); @@ -18,14 +37,13 @@ export const DashboardSidebar = ({ location }: DashboardSidebarProps) => { } = useCardStyles(false); return ( - +
@@ -34,4 +52,4 @@ export const DashboardSidebar = ({ location }: DashboardSidebarProps) => { }; const useMinRowForFullHeight = (wrapperRef: RefObject) => - wrapperRef.current ? Math.floor(wrapperRef.current!.offsetHeight / 64) : 2; + wrapperRef.current ? Math.floor(wrapperRef.current!.offsetHeight / 128) : 2; diff --git a/src/components/Dashboard/Wrappers/WrapperContent.tsx b/src/components/Dashboard/Wrappers/WrapperContent.tsx index 579bbd7c8..cfb82ae39 100644 --- a/src/components/Dashboard/Wrappers/WrapperContent.tsx +++ b/src/components/Dashboard/Wrappers/WrapperContent.tsx @@ -6,6 +6,7 @@ import { IWidget, IWidgetDefinition } from '../../../widgets/widgets'; import { WidgetWrapper } from '../../../widgets/WidgetWrapper'; import { appTileDefinition } from '../Tiles/Apps/AppTile'; import { GridstackTileWrapper } from '../Tiles/TileWrapper'; +import { useGridstackStore } from './gridstack/store'; interface WrapperContentProps { apps: AppType[]; @@ -18,6 +19,10 @@ interface WrapperContentProps { } export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) { + const shapeSize = useGridstackStore((x) => x.currentShapeSize); + + if (!shapeSize) return null; + return ( <> {apps?.map((app) => { @@ -29,8 +34,8 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) { key={app.id} itemRef={refs.items.current[app.id]} {...tile} - {...app.shape.location} - {...app.shape.size} + {...app.shape[shapeSize]?.location} + {...app.shape[shapeSize]?.size} > @@ -49,8 +54,8 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) { itemRef={refs.items.current[widget.id]} id={definition.id} {...definition.gridstack} - {...widget.shape.location} - {...widget.shape.size} + {...widget.shape[shapeSize]?.location} + {...widget.shape[shapeSize]?.size} > diff --git a/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts index ba8d2d413..36836222d 100644 --- a/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts +++ b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts @@ -1,6 +1,7 @@ import { GridStack, GridStackNode } from 'fily-publish-gridstack'; import { MutableRefObject, RefObject } from 'react'; import { AppType } from '../../../../types/app'; +import { ShapeType } from '../../../../types/shape'; import { IWidget } from '../../../../widgets/widgets'; export const initializeGridstack = ( @@ -12,7 +13,8 @@ export const initializeGridstack = ( items: AppType[], widgets: IWidget[], isEditMode: boolean, - isLargerThanSm: boolean, + wrapperColumnCount: 3 | 6 | 12, + shapeSize: 'sm' | 'md' | 'lg', events: { onChange: (changedNode: GridStackNode) => void; onAdd: (addedNode: GridStackNode) => void; @@ -20,27 +22,29 @@ export const initializeGridstack = ( ) => { if (!wrapperRef.current) return; // calculates the currently available count of columns - const columnCount = - areaType === 'sidebar' ? 1 : isLargerThanSm || typeof isLargerThanSm === 'undefined' ? 12 : 6; - const minRow = areaType !== 'sidebar' ? 1 : Math.floor(wrapperRef.current.offsetHeight / 64); + const columnCount = areaType === 'sidebar' ? 2 : wrapperColumnCount; + const minRow = areaType !== 'sidebar' ? 1 : Math.floor(wrapperRef.current.offsetHeight / 128); // initialize gridstack const newGrid = gridRef; newGrid.current = GridStack.init( { column: columnCount, margin: 10, - cellHeight: 64, + cellHeight: 128, float: true, alwaysShowResizeHandle: 'mobile', acceptWidgets: true, disableOneColumnMode: true, staticGrid: !isEditMode, minRow, + animate: false, }, // selector of the gridstack item (it's eather category or wrapper) `.grid-stack-${areaType}[data-${areaType}='${areaId}']` ); const grid = newGrid.current; + // Must be used to update the column count after the initialization + grid.column(columnCount); // Add listener for moving items around in a wrapper grid.on('change', (_, el) => { @@ -60,13 +64,23 @@ export const initializeGridstack = ( grid.batchUpdate(); grid.removeAll(false); - items.forEach( - ({ id }) => - itemRefs.current[id] && grid.makeWidget(itemRefs.current[id].current as HTMLDivElement) - ); - widgets.forEach( - ({ id }) => - itemRefs.current[id] && grid.makeWidget(itemRefs.current[id].current as HTMLDivElement) - ); + items.forEach(({ id, shape }) => { + const item = itemRefs.current[id]?.current; + setAttributesFromShape(item, shape[shapeSize]); + item && grid.makeWidget(item as HTMLDivElement); + }); + widgets.forEach(({ id, shape }) => { + const item = itemRefs.current[id]?.current; + setAttributesFromShape(item, shape[shapeSize]); + item && grid.makeWidget(item as HTMLDivElement); + }); grid.batchUpdate(false); }; + +function setAttributesFromShape(ref: HTMLDivElement | null, sizedShape: ShapeType['lg']) { + if (!sizedShape || !ref) return; + ref.setAttribute('gs-x', sizedShape.location.x.toString()); + ref.setAttribute('gs-y', sizedShape.location.y.toString()); + ref.setAttribute('gs-w', sizedShape.size.width.toString()); + ref.setAttribute('gs-h', sizedShape.size.height.toString()); +} diff --git a/src/components/Dashboard/Wrappers/gridstack/store.tsx b/src/components/Dashboard/Wrappers/gridstack/store.tsx new file mode 100644 index 000000000..881c0ee39 --- /dev/null +++ b/src/components/Dashboard/Wrappers/gridstack/store.tsx @@ -0,0 +1,31 @@ +import { useMantineTheme } from '@mantine/core'; +import create from 'zustand'; + +export const useGridstackStore = create((set, get) => ({ + mainAreaWidth: null, + currentShapeSize: null, + setMainAreaWidth: (w: number) => + set((v) => ({ ...v, mainAreaWidth: w, currentShapeSize: getCurrentShapeSize(w) })), +})); + +interface GridstackStoreType { + mainAreaWidth: null | number; + currentShapeSize: null | 'sm' | 'md' | 'lg'; + setMainAreaWidth: (width: number) => void; +} + +export const useWrapperColumnCount = () => { + const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth); + const { sm, xl } = useMantineTheme().breakpoints; + if (!mainAreaWidth) return null; + + if (mainAreaWidth >= xl) return 12; + + if (mainAreaWidth >= sm) return 6; + + return 3; +}; + +function getCurrentShapeSize(size: number) { + return size >= 1400 ? 'lg' : size >= 768 ? 'md' : 'sm'; +} diff --git a/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts index b6e7fbf64..a47f07486 100644 --- a/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts +++ b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts @@ -1,22 +1,13 @@ import { GridStack, GridStackNode } from 'fily-publish-gridstack'; -import { - createRef, - MutableRefObject, - RefObject, - useEffect, - useLayoutEffect, - useMemo, - useRef, -} from 'react'; +import { createRef, MutableRefObject, RefObject, useEffect, useMemo, useRef } from 'react'; import { useConfigContext } from '../../../../config/provider'; import { useConfigStore } from '../../../../config/store'; -import { useResize } from '../../../../hooks/use-resize'; -import { useScreenLargerThan } from '../../../../hooks/useScreenLargerThan'; import { AppType } from '../../../../types/app'; import { AreaType } from '../../../../types/area'; import { IWidget } from '../../../../widgets/widgets'; import { useEditModeStore } from '../../Views/useEditModeStore'; import { initializeGridstack } from './init-gridstack'; +import { useGridstackStore, useWrapperColumnCount } from './store'; interface UseGristackReturnType { apps: AppType[]; @@ -32,7 +23,6 @@ export const useGridstack = ( areaType: 'wrapper' | 'category' | 'sidebar', areaId: string ): UseGristackReturnType => { - const isLargerThanSm = useScreenLargerThan('sm'); const isEditMode = useEditModeStore((x) => x.enabled); const { config, configVersion, name: configName } = useConfigContext(); const updateConfig = useConfigStore((x) => x.updateConfig); @@ -42,8 +32,15 @@ export const useGridstack = ( const itemRefs = useRef>>({}); // reference of the gridstack object for modifications after initialization const gridRef = useRef(); + const wrapperColumnCount = useWrapperColumnCount(); + const shapeSize = useGridstackStore((x) => x.currentShapeSize); + const mainAreaWidth = useGridstackStore((x) => x.mainAreaWidth); // width of the wrapper (updating on page resize) - const { width, height } = useResize(wrapperRef); + const root: HTMLHtmlElement = useMemo(() => document.querySelector(':root')!, []); + + if (!mainAreaWidth || !shapeSize || !wrapperColumnCount) { + throw new Error('UseGridstack should not be executed before mainAreaWidth has been set!'); + } const items = useMemo( () => @@ -77,55 +74,17 @@ export const useGridstack = ( }); } - // change column count depending on the width and the gridRef useEffect(() => { - if (areaType === 'sidebar') return; - gridRef.current?.column( - isLargerThanSm || typeof isLargerThanSm === 'undefined' ? 12 : 6, - (column, prevColumn, newNodes, nodes) => { - let nextRow = 0; - let available = 6; + const widgetWidth = mainAreaWidth / wrapperColumnCount; + // widget width is used to define sizes of gridstack items within global.scss + root.style.setProperty('--gridstack-widget-width', widgetWidth.toString()); + gridRef.current?.cellHeight(widgetWidth); + }, [mainAreaWidth, wrapperColumnCount, gridRef.current]); - if (column === prevColumn) { - newNodes.concat(nodes); - return; - } - - nodes.reverse().forEach((node) => { - const newnode = node; - const width = parseInt(newnode.el!.getAttribute('data-gridstack-w')!, 10); - const height = parseInt(newnode.el!.getAttribute('data-gridstack-h')!, 10); - const x = parseInt(newnode.el!.getAttribute('data-gridstack-x')!, 10); - const y = parseInt(newnode.el!.getAttribute('data-gridstack-y')!, 10); - - if (column === 6) { - newnode.x = available >= width ? 6 - available : 0; - newnode.y = nextRow; - - if (width > 6) { - newnode.w = 6; - nextRow += 2; - available = 6; - } else if (available >= width) { - available -= width; - if (available === 0) { - nextRow += 2; - available = 6; - } - } else if (available < width) { - newnode.y = newnode.y! + 2; - available = 6 - width; - nextRow += 2; - } - } else { - newnode.x = y % 2 === 1 ? x + 6 : x; - newnode.y = Math.floor(y / 2); - } - newNodes.push(newnode); - }); - } - ); - }, [isLargerThanSm]); + useEffect(() => { + // column count is used to define count of columns of gridstack within global.scss + root.style.setProperty('--gridstack-column-count', wrapperColumnCount.toString()); + }, [wrapperColumnCount]); const onChange = isEditMode ? (changedNode: GridStackNode) => { @@ -143,14 +102,14 @@ export const useGridstack = ( : previous.widgets.find((x) => x.id === itemId); if (!currentItem) return previous; - currentItem.shape = { + currentItem.shape[shapeSize] = { location: { - x: changedNode.x ?? currentItem.shape.location.x, - y: changedNode.y ?? currentItem.shape.location.y, + x: changedNode.x ?? currentItem.shape[shapeSize].location.x, + y: changedNode.y ?? currentItem.shape[shapeSize].location.y, }, size: { - width: changedNode.w ?? currentItem.shape.size.width, - height: changedNode.h ?? currentItem.shape.size.height, + width: changedNode.w ?? currentItem.shape[shapeSize].size.width, + height: changedNode.h ?? currentItem.shape[shapeSize].size.height, }, }; @@ -209,14 +168,14 @@ export const useGridstack = ( }; } - currentItem.shape = { + currentItem.shape[shapeSize] = { location: { - x: addedNode.x ?? currentItem.shape.location.x, - y: addedNode.y ?? currentItem.shape.location.y, + x: addedNode.x ?? currentItem.shape[shapeSize].location.x, + y: addedNode.y ?? currentItem.shape[shapeSize].location.y, }, size: { - width: addedNode.w ?? currentItem.shape.size.width, - height: addedNode.h ?? currentItem.shape.size.height, + width: addedNode.w ?? currentItem.shape[shapeSize].size.width, + height: addedNode.h ?? currentItem.shape[shapeSize].size.height, }, }; @@ -272,7 +231,7 @@ export const useGridstack = ( : () => {}; // initialize the gridstack - useLayoutEffect(() => { + useEffect(() => { initializeGridstack( areaType, wrapperRef, @@ -282,13 +241,14 @@ export const useGridstack = ( items, widgets ?? [], isEditMode, - isLargerThanSm, + wrapperColumnCount, + shapeSize, { onChange, onAdd, } ); - }, [items, wrapperRef.current, widgets]); + }, [items, wrapperRef.current, widgets, wrapperColumnCount]); return { apps: items, diff --git a/src/components/Settings/Customization/CustomizationSettings.tsx b/src/components/Settings/Customization/CustomizationSettings.tsx index 8265ac4a6..bc95fc785 100644 --- a/src/components/Settings/Customization/CustomizationSettings.tsx +++ b/src/components/Settings/Customization/CustomizationSettings.tsx @@ -1,4 +1,4 @@ -import { ScrollArea, Stack } from '@mantine/core'; +import { Button, ScrollArea, Stack } from '@mantine/core'; import { useViewportSize } from '@mantine/hooks'; import { useTranslation } from 'next-i18next'; import { useConfigContext } from '../../../config/provider'; diff --git a/src/config/store.ts b/src/config/store.ts index 9f2fc9633..4d32ef860 100644 --- a/src/config/store.ts +++ b/src/config/store.ts @@ -75,7 +75,7 @@ interface UseConfigStoreType { updateConfig: ( name: string, updateCallback: (previous: ConfigType) => ConfigType, - shouldRegenerateGridstace?: + shouldRegenerateGridstack?: | boolean | ((previousConfig: ConfigType, currentConfig: ConfigType) => boolean), shouldSaveConfigToFileSystem?: boolean diff --git a/src/hooks/use-resize.ts b/src/hooks/use-resize.ts index 68a2d7a52..e6f632d36 100644 --- a/src/hooks/use-resize.ts +++ b/src/hooks/use-resize.ts @@ -1,12 +1,13 @@ -import { useCallback, useEffect, useState, MutableRefObject } from 'react'; +import { MutableRefObject, useCallback, useEffect, useState } from 'react'; -export const useResize = (myRef: MutableRefObject) => { +export const useResize = (myRef: MutableRefObject, dependencies: any[]) => { const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); const handleResize = useCallback(() => { - setWidth(myRef.current?.offsetWidth ?? 0); - setHeight(myRef.current?.offsetHeight ?? 0); + if (!myRef.current) return; + setWidth(myRef.current.offsetWidth); + setHeight(myRef.current.offsetHeight); }, [myRef]); useEffect(() => { @@ -19,5 +20,9 @@ export const useResize = (myRef: MutableRefObject) => { }; }, [myRef, handleResize]); + useEffect(() => { + handleResize(); + }, [myRef, dependencies]); + return { width, height }; }; diff --git a/src/modules/Docker/ContainerActionBar.tsx b/src/modules/Docker/ContainerActionBar.tsx index 5a0ad3efd..da7388c73 100644 --- a/src/modules/Docker/ContainerActionBar.tsx +++ b/src/modules/Docker/ContainerActionBar.tsx @@ -190,13 +190,35 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction }, }, shape: { - location: { - x: 0, - y: 0, + sm: { + location: { + x: 0, + y: 0, + }, + size: { + width: appTileDefinition.minWidth, + height: appTileDefinition.minHeight, + }, }, - size: { - width: appTileDefinition.minWidth, - height: appTileDefinition.minHeight, + md: { + location: { + x: 0, + y: 0, + }, + size: { + width: appTileDefinition.minWidth, + height: appTileDefinition.minHeight, + }, + }, + lg: { + location: { + x: 0, + y: 0, + }, + size: { + width: appTileDefinition.minWidth, + height: appTileDefinition.minHeight, + }, }, }, integration: { diff --git a/src/styles/global.scss b/src/styles/global.scss index a000ae014..0c1fe9d34 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -1,5 +1,10 @@ @import 'fily-publish-gridstack/dist/gridstack.min.css'; +:root { + --gridstack-widget-width: 64; + --gridstack-column-count: 12; +} + .grid-stack-placeholder > .placeholder-content { background-color: rgb(248, 249, 250) !important; border-radius: 12px; @@ -12,27 +17,59 @@ } } +// Styling for grid-stack main area @for $i from 1 to 13 { - .grid-stack>.grid-stack-item[gs-w="#{$i}"] { width: ($i / 12) * 100 + "%" } - .grid-stack>.grid-stack-item[gs-min-w="#{$i}"] { min-width: ($i / 12) * 100 + "%" } - .grid-stack>.grid-stack-item[gs-max-w="#{$i}"] { max-width: ($i / 12) * 100 + "%" } + .grid-stack>.grid-stack-item[gs-w="#{$i}"] { width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) } + .grid-stack>.grid-stack-item[gs-min-w="#{$i}"] { min-width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) } + .grid-stack>.grid-stack-item[gs-max-w="#{$i}"] { max-width: calc(100% / #{var(--gridstack-column-count)} * #{$i}) } } @for $i from 1 to 96 { - .grid-stack>.grid-stack-item[gs-h="#{$i}"] { height: $i * 64 + "px" } - .grid-stack>.grid-stack-item[gs-min-h="#{$i}"] { min-height: $i * 64 + "px" } - .grid-stack>.grid-stack-item[gs-max-h="#{$i}"] { max-height: $i * 64 + "px" } + .grid-stack>.grid-stack-item[gs-h="#{$i}"] { height: calc(#{$i}px * #{var(--gridstack-widget-width)}) } + .grid-stack>.grid-stack-item[gs-min-h="#{$i}"] { min-height: calc(#{$i}px * #{var(--gridstack-widget-width)}) } + .grid-stack>.grid-stack-item[gs-max-h="#{$i}"] { max-height: calc(#{$i}px * #{var(--gridstack-widget-width)}) } } @for $i from 1 to 13 { - .grid-stack>.grid-stack-item[gs-x="#{$i}"] { left: ($i / 12) * 100 + "%" } + .grid-stack>.grid-stack-item[gs-x="#{$i}"] { left: calc(100% / #{var(--gridstack-column-count)} * #{$i}) } } @for $i from 1 to 96 { - .grid-stack>.grid-stack-item[gs-y="#{$i}"] { top: $i * 64 + "px" } + .grid-stack>.grid-stack-item[gs-y="#{$i}"] { top: calc(#{$i}px * #{var(--gridstack-widget-width)}) } } +.grid-stack>.grid-stack-item { + min-width: calc(percentage(1) * #{var(--gridstack-widget-width)}); +} + +// Styling for sidebar grid-stack elements +@for $i from 1 to 3 { + .grid-stack.grid-stack-sidebar>.grid-stack-item[gs-w="#{$i}"] { width: 128px * $i } + .grid-stack.grid-stack-sidebar>.grid-stack-item[gs-min-w="#{$i}"] { min-width: 128px * $i } + .grid-stack.grid-stack-sidebar>.grid-stack-item[gs-max-w="#{$i}"] { max-width: 128px * $i } +} + +@for $i from 1 to 96 { + .grid-stack.grid-stack-sidebar>.grid-stack-item[gs-h="#{$i}"] { height: 128px * $i } + .grid-stack.grid-stack-sidebar>.grid-stack-item[gs-min-h="#{$i}"] { min-height: 128px * $i } + .grid-stack.grid-stack-sidebar>.grid-stack-item[gs-max-h="#{$i}"] { max-height: 128px * $i } +} + +@for $i from 1 to 13 { + .grid-stack.grid-stack-sidebar>.grid-stack-item[gs-x="#{$i}"] { left: 128px * $i } +} + + +@for $i from 1 to 96 { + .grid-stack.grid-stack-sidebar>.grid-stack-item[gs-y="#{$i}"] { top: 128px * $i } +} + +.grid-stack.grid-stack-sidebar>.grid-stack-item { + min-width: 128px; +} + +// General gridstack styling .grid-stack>.grid-stack-item>.grid-stack-item-content, .grid-stack>.grid-stack-item>.placeholder-content { inset: 10px; @@ -43,10 +80,6 @@ right: 10px; } -.grid-stack>.grid-stack-item { - min-width: (1/12)+'%'; -} - .grid-stack > .grid-stack-item > .grid-stack-item-content { overflow-y: auto; } @@ -54,19 +87,3 @@ .grid-stack.grid-stack-animate { transition: none; } - -@media screen and (max-width: 768px) { - @for $i from 1 to 7 { - .grid-stack>.grid-stack-item[gs-w="#{$i}"] { width: percentage(($i / 6)) !important } - .grid-stack>.grid-stack-item[gs-min-w="#{$i}"] { min-width: percentage(($i / 6)) !important } - .grid-stack>.grid-stack-item[gs-max-w="#{$i}"] { max-width: percentage(($i / 6)) !important } - } - - @for $i from 1 to 7 { - .grid-stack>.grid-stack-item[gs-x="#{$i}"] { left: percentage(($i / 6)) } - } - - .grid-stack>.grid-stack-item { - min-width: percentage(1/6) !important; - } -} \ No newline at end of file diff --git a/src/tools/config/migrateConfig.ts b/src/tools/config/migrateConfig.ts index 9648c1fd1..13d811523 100644 --- a/src/tools/config/migrateConfig.ts +++ b/src/tools/config/migrateConfig.ts @@ -92,6 +92,17 @@ const getConfigAndCreateIfNotExsists = ( return category; }; +const getShapeForColumnCount = (index: number, columnCount: number) => ({ + location: { + x: index % columnCount, + y: Math.floor(index / columnCount), + }, + size: { + width: 1, + height: 1, + }, +}); + const migrateService = ( oldService: serviceItem, serviceIndex: number, @@ -117,13 +128,8 @@ const migrateService = ( }, area: areaType, shape: { - location: { - x: (serviceIndex * 3) % 18, - y: Math.floor(serviceIndex / 6) * 3, - }, - size: { - width: 3, - height: 3, - }, + lg: getShapeForColumnCount(serviceIndex, 12), + md: getShapeForColumnCount(serviceIndex, 6), + sm: getShapeForColumnCount(serviceIndex, 3), }, }); diff --git a/src/types/shape.ts b/src/types/shape.ts index 0bc27df0e..a8a6e6823 100644 --- a/src/types/shape.ts +++ b/src/types/shape.ts @@ -1,4 +1,10 @@ export interface ShapeType { + lg?: SizedShapeType; + md?: SizedShapeType; + sm?: SizedShapeType; +} + +export interface SizedShapeType { location: { x: number; y: number; diff --git a/src/widgets/date/DateTile.tsx b/src/widgets/date/DateTile.tsx index a7aee4fc9..5e830a792 100644 --- a/src/widgets/date/DateTile.tsx +++ b/src/widgets/date/DateTile.tsx @@ -17,7 +17,7 @@ const definition = defineWidget({ }, gridstack: { minWidth: 2, - minHeight: 2, + minHeight: 1, maxWidth: 12, maxHeight: 12, }, diff --git a/src/widgets/weather/WeatherTile.tsx b/src/widgets/weather/WeatherTile.tsx index 677d4bd6c..b053a0a2a 100644 --- a/src/widgets/weather/WeatherTile.tsx +++ b/src/widgets/weather/WeatherTile.tsx @@ -20,7 +20,7 @@ const definition = defineWidget({ }, gridstack: { minWidth: 2, - minHeight: 2, + minHeight: 1, maxWidth: 12, maxHeight: 12, },