([]);
+ const setHeight = useCallback((index: number, height: number) => {
+ setHeights((current) => {
+ if (current[index] === height) {
+ return current;
+ }
+ const next = [...current];
+ next[index] = height;
+ return next;
+ });
+ }, []);
+ const elements = useMemo(() => {
+ return Children.toArray(children).map((child, index) => {
+ const setItemHeight = (height: number) => {
+ setHeight(index, height);
+ };
+ return (
+
+ {child}
+
+ );
+ });
+ }, [children, setHeight]);
+
+ const layout = useMemo(() => {
+ const columnHeights = Array.from({ length: columns }).map(() => 0);
+ return heights.map((height) => {
+ const lowestColumn = columnHeights.indexOf(Math.min(...columnHeights));
+ const currentHeight = columnHeights[lowestColumn];
+ columnHeights[lowestColumn] += height + gutter;
+ const y = currentHeight;
+ const margin = gutter / 2;
+ const marginLeft = lowestColumn === 0 ? 0 : margin;
+ const marginRight = lowestColumn === columns - 1 ? 0 : margin;
+ const x = lowestColumn * columnWidth + marginLeft;
+ const width = columnWidth - marginLeft - marginRight;
+ return { x, y, width };
+ });
+ }, [heights, columns, columnWidth]);
+
+ const updateSize = useCallback(() => {
+ if (!ref.current) {
+ return;
+ }
+ const nextWidth = ref.current.getBoundingClientRect().width;
+ const nextColumns = Math.max(Math.floor(nextWidth / maxColumnWidth), 1);
+ const nextColumnWidth = nextWidth / nextColumns;
+ setColumns(nextColumns);
+ setColumnWidth(nextColumnWidth);
+ }, []);
+
+ useLayoutEffect(() => {
+ if (!ref.current) {
+ return;
+ }
+ const observer = new ResizeObserver(() => {
+ updateSize();
+ });
+ observer.observe(ref.current);
+ return () => {
+ observer.disconnect();
+ };
+ }, [updateSize]);
+
+ const debouncedLayout = useDebounce(layout, 10);
+
+ return (
+
+ {elements.map((element, index) => (
+
+ {element}
+
+ ))}
+
+ );
+};
+
+export { Masonry };
diff --git a/packages/ui/src/base/masonry/item.tsx b/packages/ui/src/base/masonry/item.tsx
new file mode 100644
index 0000000..45a622b
--- /dev/null
+++ b/packages/ui/src/base/masonry/item.tsx
@@ -0,0 +1,29 @@
+import { useLayoutEffect, useRef } from 'react';
+
+type Props = {
+ children: React.ReactNode;
+ setHeight: (height: number) => void;
+};
+
+const MasonryItem: React.FC = ({ children, setHeight }) => {
+ const ref = useRef(null);
+
+ useLayoutEffect(() => {
+ if (!ref.current) {
+ return;
+ }
+ const height = ref.current.getBoundingClientRect().height;
+ setHeight(height);
+ const observer = new ResizeObserver((entries) => {
+ setHeight(entries[0].contentRect.height);
+ });
+ observer.observe(ref.current);
+ return () => {
+ observer.disconnect();
+ };
+ }, [setHeight]);
+
+ return {children}
;
+};
+
+export { MasonryItem };
diff --git a/packages/ui/src/interface/board/index.tsx b/packages/ui/src/interface/board/index.tsx
index dfbb6a5..b8f4919 100644
--- a/packages/ui/src/interface/board/index.tsx
+++ b/packages/ui/src/interface/board/index.tsx
@@ -5,7 +5,7 @@ import {
useUpdateWidget,
} from '@refocus/sdk';
import { IoAddCircleOutline } from 'react-icons/io5';
-import { View } from '../../base';
+import { Masonry, View } from '../../base';
import { Widget } from '../widget';
import { AddWidgetFromUrl } from '../add-from-url';
import { styled } from 'styled-components';
@@ -15,14 +15,12 @@ type BoardProps = {
id: string;
};
-const Wrapper = styled(View)`
- flex-wrap: wrap;
-`;
-
const ItemWrapper = styled(View)`
- max-width: 400px;
overflow-y: auto;
max-height: 500px;
+ max-width: 100%;
+ box-shadow: 0 0 4px 0px ${({ theme }) => theme.colors.bg.highlight};
+ border-radius: ${({ theme }) => theme.radii.md}px;
`;
const Board: React.FC = ({ board, id }) => {
@@ -42,19 +40,21 @@ const Board: React.FC = ({ board, id }) => {
-
- {Object.entries(board.widgets).map(([widgetId, widget]) => (
-
- setWidgetData(id, widgetId, data)}
- onRemove={() => removeWidget(id, widgetId)}
- />
-
- ))}
-
+
+
+ {Object.entries(board.widgets).map(([widgetId, widget]) => (
+
+ setWidgetData(id, widgetId, data)}
+ onRemove={() => removeWidget(id, widgetId)}
+ />
+
+ ))}
+
+
);
};
diff --git a/packages/ui/src/interface/widget/index.tsx b/packages/ui/src/interface/widget/index.tsx
index 2024afe..e49d84d 100644
--- a/packages/ui/src/interface/widget/index.tsx
+++ b/packages/ui/src/interface/widget/index.tsx
@@ -23,6 +23,12 @@ const Wrapper = styled(View)`
background: ${({ theme }) => theme.colors.bg.base};
`;
+const WidgetWrapper = styled(View)`
+ flex-grow: 0;
+ overflow: hidden;
+ flex: 1;
+`;
+
const Widget: React.FC = ({
id,
data,
@@ -47,9 +53,9 @@ const Widget: React.FC = ({
return (
-
+
-
+
{hasMenu && (
diff --git a/packages/ui/src/typography/index.tsx b/packages/ui/src/typography/index.tsx
index 015dcd5..4fa732b 100644
--- a/packages/ui/src/typography/index.tsx
+++ b/packages/ui/src/typography/index.tsx
@@ -33,6 +33,7 @@ const styles = {
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
+ break-word: break-all;
`,
} satisfies Record>;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 69c34d0..835da99 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -147,9 +147,6 @@ importers:
packages/ui:
dependencies:
- '@refocus/sdk':
- specifier: workspace:^
- version: link:../sdk
'@radix-ui/react-dialog':
specifier: ^1.0.4
version: 1.0.4(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
@@ -159,6 +156,12 @@ importers:
'@radix-ui/react-tabs':
specifier: ^1.0.4
version: 1.0.4(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
+ '@refocus/sdk':
+ specifier: workspace:^
+ version: link:../sdk
+ framer-motion:
+ specifier: ^10.12.16
+ version: 10.12.16(react-dom@18.2.0)(react@18.2.0)
react-icons:
specifier: ^4.9.0
version: 4.9.0(react@18.2.0)
@@ -168,6 +171,9 @@ importers:
styled-components:
specifier: 6.0.0-rc.3
version: 6.0.0-rc.3(react-dom@18.2.0)(react@18.2.0)
+ usehooks-ts:
+ specifier: ^2.9.1
+ version: 2.9.1(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@refocus/config':
specifier: workspace:^
@@ -10524,6 +10530,17 @@ packages:
tslib: 2.5.3
dev: false
+ /usehooks-ts@2.9.1(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==}
+ engines: {node: '>=16.15.0', npm: '>=8'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true