위젯영역 리사이즈
This commit is contained in:
@@ -120,6 +120,12 @@ const FloatingWidgetLayer: React.FC<Props> = ({
|
|||||||
)));
|
)));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleUpdateGridFr = useCallback((id: string, rowFr: number[], colFr: number[]) => {
|
||||||
|
setInstances(prev => prev.map(inst => (
|
||||||
|
inst.id === id ? { ...inst, rowFr, colFr } : inst
|
||||||
|
)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onInstancesChange?.(instances.length);
|
onInstancesChange?.(instances.length);
|
||||||
}, [instances.length, onInstancesChange]);
|
}, [instances.length, onInstancesChange]);
|
||||||
@@ -154,6 +160,7 @@ const FloatingWidgetLayer: React.FC<Props> = ({
|
|||||||
onFocus={() => bringToFront(inst.id)}
|
onFocus={() => bringToFront(inst.id)}
|
||||||
onUpdateSlots={slots => handleUpdateSlots(inst.id, slots)}
|
onUpdateSlots={slots => handleUpdateSlots(inst.id, slots)}
|
||||||
onUpdateSize={(w, h) => handleUpdateSize(inst.id, w, h)}
|
onUpdateSize={(w, h) => handleUpdateSize(inst.id, w, h)}
|
||||||
|
onUpdateGridFr={(rowFr, colFr) => handleUpdateGridFr(inst.id, rowFr, colFr)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useDraggablePanel } from '../../hooks/useDraggablePanel';
|
import { useDraggablePanel } from '../../hooks/useDraggablePanel';
|
||||||
|
import { useFloatingWidgetGridSplit } from '../../hooks/useFloatingWidgetGridSplit';
|
||||||
import { useFloatingWidgetResize } from '../../hooks/useFloatingWidgetResize';
|
import { useFloatingWidgetResize } from '../../hooks/useFloatingWidgetResize';
|
||||||
import { minSizeForGrid, type FloatingWidgetInstance } from '../../types/floatingWidget';
|
import {
|
||||||
|
defaultGridFr,
|
||||||
|
gridSplitPositions,
|
||||||
|
gridTemplateFr,
|
||||||
|
minSizeForGrid,
|
||||||
|
type FloatingWidgetInstance,
|
||||||
|
} from '../../types/floatingWidget';
|
||||||
import type { WidgetSlot } from '../../types/widgetDashboard';
|
import type { WidgetSlot } from '../../types/widgetDashboard';
|
||||||
import WidgetSlotView from '../widgetDashboard/WidgetSlotView';
|
import WidgetSlotView from '../widgetDashboard/WidgetSlotView';
|
||||||
import WidgetPickerModal from '../widgetDashboard/WidgetPickerModal';
|
import WidgetPickerModal from '../widgetDashboard/WidgetPickerModal';
|
||||||
@@ -14,6 +21,7 @@ interface Props {
|
|||||||
onFocus: () => void;
|
onFocus: () => void;
|
||||||
onUpdateSlots: (slots: WidgetSlot[]) => void;
|
onUpdateSlots: (slots: WidgetSlot[]) => void;
|
||||||
onUpdateSize: (width: number, height: number) => void;
|
onUpdateSize: (width: number, height: number) => void;
|
||||||
|
onUpdateGridFr: (rowFr: number[], colFr: number[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FloatingWidgetWindow: React.FC<Props> = ({
|
const FloatingWidgetWindow: React.FC<Props> = ({
|
||||||
@@ -23,9 +31,23 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
onFocus,
|
onFocus,
|
||||||
onUpdateSlots,
|
onUpdateSlots,
|
||||||
onUpdateSize,
|
onUpdateSize,
|
||||||
|
onUpdateGridFr,
|
||||||
}) => {
|
}) => {
|
||||||
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
|
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
|
||||||
const bodyRef = useRef<HTMLDivElement>(null);
|
const bodyRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [bodySize, setBodySize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
const rowFr = instance.rowFr?.length === instance.rows
|
||||||
|
? instance.rowFr
|
||||||
|
: defaultGridFr(instance.rows);
|
||||||
|
const colFr = instance.colFr?.length === instance.cols
|
||||||
|
? instance.colFr
|
||||||
|
: defaultGridFr(instance.cols);
|
||||||
|
|
||||||
|
const rowFrRef = useRef(rowFr);
|
||||||
|
const colFrRef = useRef(colFr);
|
||||||
|
rowFrRef.current = rowFr;
|
||||||
|
colFrRef.current = colFr;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
panelRef,
|
panelRef,
|
||||||
@@ -59,6 +81,25 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
handleResize,
|
handleResize,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleColFrChange = useCallback((next: number[]) => {
|
||||||
|
onUpdateGridFr(rowFrRef.current, next);
|
||||||
|
}, [onUpdateGridFr]);
|
||||||
|
|
||||||
|
const handleRowFrChange = useCallback((next: number[]) => {
|
||||||
|
onUpdateGridFr(next, colFrRef.current);
|
||||||
|
}, [onUpdateGridFr]);
|
||||||
|
|
||||||
|
const { onColSplitPointerDown, onRowSplitPointerDown } = useFloatingWidgetGridSplit({
|
||||||
|
onColFrChange: handleColFrChange,
|
||||||
|
onRowFrChange: handleRowFrChange,
|
||||||
|
getColFr: () => colFrRef.current,
|
||||||
|
getRowFr: () => rowFrRef.current,
|
||||||
|
getBodySize: () => ({
|
||||||
|
width: bodyRef.current?.clientWidth ?? bodySize.width,
|
||||||
|
height: bodyRef.current?.clientHeight ?? bodySize.height,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const updateSlot = useCallback((slotId: string, updater: (s: WidgetSlot) => WidgetSlot) => {
|
const updateSlot = useCallback((slotId: string, updater: (s: WidgetSlot) => WidgetSlot) => {
|
||||||
onUpdateSlots(instance.slots.map(s => (s.id === slotId ? updater(s) : s)));
|
onUpdateSlots(instance.slots.map(s => (s.id === slotId ? updater(s) : s)));
|
||||||
}, [instance.slots, onUpdateSlots]);
|
}, [instance.slots, onUpdateSlots]);
|
||||||
@@ -73,23 +114,35 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
updateSlot(slotId, s => ({ ...s, widgetType: null, config: {} }));
|
updateSlot(slotId, s => ({ ...s, widgetType: null, config: {} }));
|
||||||
}, [updateSlot]);
|
}, [updateSlot]);
|
||||||
|
|
||||||
/* 창 크기 변경 시 내부 차트·위젯 레이아웃 재동기화 */
|
/* 창·그리드 크기 변경 시 내부 차트·위젯 레이아웃 재동기화 */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = bodyRef.current;
|
const el = bodyRef.current;
|
||||||
if (!el) return undefined;
|
if (!el) return undefined;
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
const ro = new ResizeObserver(() => {
|
const scheduleLayoutSync = () => {
|
||||||
|
setBodySize({ width: el.clientWidth, height: el.clientHeight });
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
window.dispatchEvent(new CustomEvent('gc-widget-layout-sync'));
|
window.dispatchEvent(new CustomEvent('gc-widget-layout-sync'));
|
||||||
}, 80);
|
}, 80);
|
||||||
});
|
};
|
||||||
|
scheduleLayoutSync();
|
||||||
|
const ro = new ResizeObserver(() => scheduleLayoutSync());
|
||||||
ro.observe(el);
|
ro.observe(el);
|
||||||
return () => {
|
return () => {
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [instance.rows, instance.cols]);
|
||||||
|
|
||||||
|
const colSplitPositions = useMemo(
|
||||||
|
() => gridSplitPositions(colFr, bodySize.width, 1),
|
||||||
|
[colFr, bodySize.width],
|
||||||
|
);
|
||||||
|
const rowSplitPositions = useMemo(
|
||||||
|
() => gridSplitPositions(rowFr, bodySize.height, 1),
|
||||||
|
[rowFr, bodySize.height],
|
||||||
|
);
|
||||||
|
|
||||||
const title = `위젯 ${instance.rows}×${instance.cols}`;
|
const title = `위젯 ${instance.rows}×${instance.cols}`;
|
||||||
|
|
||||||
@@ -125,12 +178,13 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="fw-window-body-wrap">
|
||||||
<div
|
<div
|
||||||
ref={bodyRef}
|
ref={bodyRef}
|
||||||
className="fw-window-body"
|
className="fw-window-body"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateRows: `repeat(${instance.rows}, minmax(0, 1fr))`,
|
gridTemplateRows: gridTemplateFr(rowFr),
|
||||||
gridTemplateColumns: `repeat(${instance.cols}, minmax(0, 1fr))`,
|
gridTemplateColumns: gridTemplateFr(colFr),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{instance.slots.map(slot => (
|
{instance.slots.map(slot => (
|
||||||
@@ -145,6 +199,34 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(colSplitPositions.length > 0 || rowSplitPositions.length > 0) && (
|
||||||
|
<div className="fw-grid-split-layer" aria-hidden="true">
|
||||||
|
{colSplitPositions.map((left, index) => (
|
||||||
|
<div
|
||||||
|
key={`col-${index}`}
|
||||||
|
className="fw-grid-splitter fw-grid-splitter--v"
|
||||||
|
style={{ left }}
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-label={`열 ${index + 1}·${index + 2} 경계 조절`}
|
||||||
|
onPointerDown={onColSplitPointerDown(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{rowSplitPositions.map((top, index) => (
|
||||||
|
<div
|
||||||
|
key={`row-${index}`}
|
||||||
|
className="fw-grid-splitter fw-grid-splitter--h"
|
||||||
|
style={{ top }}
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="horizontal"
|
||||||
|
aria-label={`행 ${index + 1}·${index + 2} 경계 조절`}
|
||||||
|
onPointerDown={onRowSplitPointerDown(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="fw-window-resize-layer">
|
<div className="fw-window-resize-layer">
|
||||||
<div
|
<div
|
||||||
className="fw-window-resize-handle fw-window-resize-handle--sw"
|
className="fw-window-resize-handle fw-window-resize-handle--sw"
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
adjustAdjacentGridFr,
|
||||||
|
FLOATING_WIDGET_CELL_MIN_H,
|
||||||
|
FLOATING_WIDGET_CELL_MIN_W,
|
||||||
|
FLOATING_WIDGET_GRID_GAP,
|
||||||
|
} from '../types/floatingWidget';
|
||||||
|
import { bindWindowDrag, isPrimaryPointerButton } from '../utils/pointerDrag';
|
||||||
|
|
||||||
|
type SplitAxis = 'col' | 'row';
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
onColFrChange: (next: number[]) => void;
|
||||||
|
onRowFrChange: (next: number[]) => void;
|
||||||
|
getColFr: () => number[];
|
||||||
|
getRowFr: () => number[];
|
||||||
|
getBodySize: () => { width: number; height: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFloatingWidgetGridSplit({
|
||||||
|
onColFrChange,
|
||||||
|
onRowFrChange,
|
||||||
|
getColFr,
|
||||||
|
getRowFr,
|
||||||
|
getBodySize,
|
||||||
|
}: Options) {
|
||||||
|
const unbindDragRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
|
const endDrag = useCallback(() => {
|
||||||
|
unbindDragRef.current?.();
|
||||||
|
unbindDragRef.current = null;
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
unbindDragRef.current?.();
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startSplit = useCallback((
|
||||||
|
e: React.PointerEvent,
|
||||||
|
axis: SplitAxis,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
if (!isPrimaryPointerButton(e)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const handle = e.currentTarget as HTMLElement;
|
||||||
|
try {
|
||||||
|
handle.setPointerCapture(e.pointerId);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
handle.classList.add('fw-grid-splitter--active');
|
||||||
|
|
||||||
|
const startX = e.clientX;
|
||||||
|
const startY = e.clientY;
|
||||||
|
const startColFr = [...getColFr()];
|
||||||
|
const startRowFr = [...getRowFr()];
|
||||||
|
const startSize = getBodySize();
|
||||||
|
|
||||||
|
document.body.style.cursor = axis === 'col' ? 'col-resize' : 'row-resize';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
|
||||||
|
unbindDragRef.current?.();
|
||||||
|
unbindDragRef.current = bindWindowDrag(
|
||||||
|
(xy) => {
|
||||||
|
if (axis === 'col') {
|
||||||
|
const delta = xy.x - startX;
|
||||||
|
onColFrChange(adjustAdjacentGridFr(
|
||||||
|
startColFr,
|
||||||
|
index,
|
||||||
|
delta,
|
||||||
|
startSize.width,
|
||||||
|
FLOATING_WIDGET_GRID_GAP,
|
||||||
|
FLOATING_WIDGET_CELL_MIN_W,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const delta = xy.y - startY;
|
||||||
|
onRowFrChange(adjustAdjacentGridFr(
|
||||||
|
startRowFr,
|
||||||
|
index,
|
||||||
|
delta,
|
||||||
|
startSize.height,
|
||||||
|
FLOATING_WIDGET_GRID_GAP,
|
||||||
|
FLOATING_WIDGET_CELL_MIN_H,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
handle.classList.remove('fw-grid-splitter--active');
|
||||||
|
endDrag();
|
||||||
|
},
|
||||||
|
{ preventTouchScroll: true },
|
||||||
|
);
|
||||||
|
}, [endDrag, getBodySize, getColFr, getRowFr, onColFrChange, onRowFrChange]);
|
||||||
|
|
||||||
|
const onColSplitPointerDown = useCallback(
|
||||||
|
(index: number) => (e: React.PointerEvent) => startSplit(e, 'col', index),
|
||||||
|
[startSplit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRowSplitPointerDown = useCallback(
|
||||||
|
(index: number) => (e: React.PointerEvent) => startSplit(e, 'row', index),
|
||||||
|
[startSplit],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { onColSplitPointerDown, onRowSplitPointerDown };
|
||||||
|
}
|
||||||
@@ -104,6 +104,14 @@
|
|||||||
background: color-mix(in srgb, #e74c3c 12%, transparent);
|
background: color-mix(in srgb, #e74c3c 12%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fw-window-body-wrap {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.fw-window-body {
|
.fw-window-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -156,6 +164,48 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 그리드 칸 경계 리사이즈 */
|
||||||
|
.fw-grid-split-layer {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-grid-splitter {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: auto;
|
||||||
|
touch-action: none;
|
||||||
|
background: transparent;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-grid-splitter--v {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 7px;
|
||||||
|
margin-left: -3px;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-grid-splitter--h {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 7px;
|
||||||
|
margin-top: -3px;
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-grid-splitter:hover,
|
||||||
|
.fw-grid-splitter--active,
|
||||||
|
.fw-window--focused .fw-grid-splitter {
|
||||||
|
background: color-mix(in srgb, var(--se-accent, var(--accent)) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-grid-splitter--active {
|
||||||
|
background: color-mix(in srgb, var(--se-accent, var(--accent)) 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
/* 리사이즈 핸들 — 좌·우 하단 (차트 툴바 위에 표시) */
|
/* 리사이즈 핸들 — 좌·우 하단 (차트 툴바 위에 표시) */
|
||||||
.fw-window-resize-layer {
|
.fw-window-resize-layer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -21,17 +21,78 @@ export const FLOATING_WIDGET_LAYOUTS: FloatingWidgetLayoutPreset[] = [
|
|||||||
{ id: '3x3', rows: 3, cols: 3, label: '3×3' },
|
{ id: '3x3', rows: 3, cols: 3, label: '3×3' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const FLOATING_WIDGET_CELL_MIN_W = 140;
|
||||||
|
export const FLOATING_WIDGET_CELL_MIN_H = 100;
|
||||||
|
export const FLOATING_WIDGET_GRID_GAP = 1;
|
||||||
|
|
||||||
export interface FloatingWidgetInstance {
|
export interface FloatingWidgetInstance {
|
||||||
id: string;
|
id: string;
|
||||||
rows: number;
|
rows: number;
|
||||||
cols: number;
|
cols: number;
|
||||||
slots: WidgetSlot[];
|
slots: WidgetSlot[];
|
||||||
|
/** 행·열 비율 (fr) — 그리드 칸 리사이즈 */
|
||||||
|
rowFr: number[];
|
||||||
|
colFr: number[];
|
||||||
position: { x: number; y: number };
|
position: { x: number; y: number };
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
zIndex: number;
|
zIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function defaultGridFr(count: number): number[] {
|
||||||
|
return Array.from({ length: count }, () => 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gridTemplateFr(fr: number[]): string {
|
||||||
|
return fr.map(f => `${Math.max(f, 0.05)}fr`).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 인접 두 트랙의 fr 비율을 픽셀 delta 만큼 조정 */
|
||||||
|
export function adjustAdjacentGridFr(
|
||||||
|
fr: number[],
|
||||||
|
index: number,
|
||||||
|
deltaPx: number,
|
||||||
|
containerPx: number,
|
||||||
|
gapPx: number,
|
||||||
|
minCellPx: number,
|
||||||
|
): number[] {
|
||||||
|
if (index < 0 || index >= fr.length - 1) return fr;
|
||||||
|
const next = [...fr];
|
||||||
|
const n = fr.length;
|
||||||
|
const totalFr = fr.reduce((a, b) => a + b, 0);
|
||||||
|
const contentPx = Math.max(0, containerPx - (n - 1) * gapPx);
|
||||||
|
const pairFr = fr[index] + fr[index + 1];
|
||||||
|
const leftPx = contentPx * fr[index] / totalFr;
|
||||||
|
const rightPx = contentPx * fr[index + 1] / totalFr;
|
||||||
|
const pairPx = leftPx + rightPx;
|
||||||
|
const minLeft = Math.min(minCellPx, pairPx / 2);
|
||||||
|
const maxLeft = Math.max(pairPx - minCellPx, pairPx / 2);
|
||||||
|
const newLeftPx = Math.max(minLeft, Math.min(maxLeft, leftPx + deltaPx));
|
||||||
|
const newRightPx = pairPx - newLeftPx;
|
||||||
|
next[index] = pairFr * (newLeftPx / pairPx);
|
||||||
|
next[index + 1] = pairFr * (newRightPx / pairPx);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 스플리터 위치(px) — gap 포함 */
|
||||||
|
export function gridSplitPositions(
|
||||||
|
fr: number[],
|
||||||
|
containerPx: number,
|
||||||
|
gapPx: number,
|
||||||
|
): number[] {
|
||||||
|
const n = fr.length;
|
||||||
|
if (n <= 1 || containerPx <= 0) return [];
|
||||||
|
const totalFr = fr.reduce((a, b) => a + b, 0);
|
||||||
|
const contentPx = Math.max(0, containerPx - (n - 1) * gapPx);
|
||||||
|
const positions: number[] = [];
|
||||||
|
let accPx = 0;
|
||||||
|
for (let i = 0; i < n - 1; i++) {
|
||||||
|
accPx += contentPx * fr[i] / totalFr;
|
||||||
|
positions.push(accPx + i * gapPx + gapPx / 2);
|
||||||
|
}
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
export function defaultSizeForGrid(rows: number, cols: number): { width: number; height: number } {
|
export function defaultSizeForGrid(rows: number, cols: number): { width: number; height: number } {
|
||||||
const cellW = 280;
|
const cellW = 280;
|
||||||
const cellH = 220;
|
const cellH = 220;
|
||||||
@@ -69,6 +130,8 @@ export function createFloatingWidgetInstance(
|
|||||||
rows,
|
rows,
|
||||||
cols,
|
cols,
|
||||||
slots: Array.from({ length: count }, () => newWidgetSlot()),
|
slots: Array.from({ length: count }, () => newWidgetSlot()),
|
||||||
|
rowFr: defaultGridFr(rows),
|
||||||
|
colFr: defaultGridFr(cols),
|
||||||
position: {
|
position: {
|
||||||
x: Math.min(80 + offset, Math.max(24, window.innerWidth - width - 24)),
|
x: Math.min(80 + offset, Math.max(24, window.innerWidth - width - 24)),
|
||||||
y: Math.min(72 + offset, Math.max(24, window.innerHeight - height - 24)),
|
y: Math.min(72 + offset, Math.max(24, window.innerHeight - height - 24)),
|
||||||
|
|||||||
Reference in New Issue
Block a user