mirror of
https://github.com/morten-olsen/refocus.dev.git
synced 2026-02-08 00:46:25 +01:00
feat: markdown, github file and UI improvements
This commit is contained in:
@@ -12,6 +12,7 @@ type BoardsContextValue = {
|
|||||||
selected?: string;
|
selected?: string;
|
||||||
boards: Boards;
|
boards: Boards;
|
||||||
addBoard: (name: string) => void;
|
addBoard: (name: string) => void;
|
||||||
|
setName: (id: string, name: string) => void;
|
||||||
selectBoard: (id: string) => void;
|
selectBoard: (id: string) => void;
|
||||||
removeBoard: (id: string) => void;
|
removeBoard: (id: string) => void;
|
||||||
addWidget: (boardId: string, type: string, data: string) => void;
|
addWidget: (boardId: string, type: string, data: string) => void;
|
||||||
@@ -46,6 +47,16 @@ const BoardsProvider: React.FC<BoardsProviderProps> = ({
|
|||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setName = useCallback((id: string, name: string) => {
|
||||||
|
setBoards((currentBoards) => ({
|
||||||
|
...currentBoards,
|
||||||
|
[id]: {
|
||||||
|
...currentBoards[id],
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const removeBoard = useCallback((id: string) => {
|
const removeBoard = useCallback((id: string) => {
|
||||||
setBoards((currentBoards) => {
|
setBoards((currentBoards) => {
|
||||||
const copy = { ...currentBoards };
|
const copy = { ...currentBoards };
|
||||||
@@ -116,6 +127,7 @@ const BoardsProvider: React.FC<BoardsProviderProps> = ({
|
|||||||
addBoard,
|
addBoard,
|
||||||
removeBoard,
|
removeBoard,
|
||||||
addWidget,
|
addWidget,
|
||||||
|
setName,
|
||||||
removeWidget,
|
removeWidget,
|
||||||
selectBoard,
|
selectBoard,
|
||||||
updateWidget,
|
updateWidget,
|
||||||
@@ -126,6 +138,7 @@ const BoardsProvider: React.FC<BoardsProviderProps> = ({
|
|||||||
addBoard,
|
addBoard,
|
||||||
removeBoard,
|
removeBoard,
|
||||||
addWidget,
|
addWidget,
|
||||||
|
setName,
|
||||||
removeWidget,
|
removeWidget,
|
||||||
selectBoard,
|
selectBoard,
|
||||||
updateWidget,
|
updateWidget,
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ const useUpdateWidget = () => {
|
|||||||
return context.updateWidget;
|
return context.updateWidget;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useSetBoardName = () => {
|
||||||
|
const context = useContext(BoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useSetBoardName must be used within a BoardsProvider');
|
||||||
|
}
|
||||||
|
return context.setName;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useBoards,
|
useBoards,
|
||||||
useSelectedBoard,
|
useSelectedBoard,
|
||||||
@@ -74,4 +82,5 @@ export {
|
|||||||
useRemoveBoard,
|
useRemoveBoard,
|
||||||
useSelectBoard,
|
useSelectBoard,
|
||||||
useUpdateWidget,
|
useUpdateWidget,
|
||||||
|
useSetBoardName,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ export {
|
|||||||
useRemoveBoard,
|
useRemoveBoard,
|
||||||
useSelectBoard,
|
useSelectBoard,
|
||||||
useUpdateWidget,
|
useUpdateWidget,
|
||||||
|
useSetBoardName,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const WidgetProvider = ({
|
|||||||
const addGlobalNotification = useNotificationAdd();
|
const addGlobalNotification = useNotificationAdd();
|
||||||
const dissmissGlobalNotification = useNotificationDismiss();
|
const dissmissGlobalNotification = useNotificationDismiss();
|
||||||
const notifications = useMemo(() => {
|
const notifications = useMemo(() => {
|
||||||
return globalNotifications.filter((n) => n.view !== ref.current);
|
return globalNotifications.filter((n) => n.view === ref.current);
|
||||||
}, [globalNotifications]);
|
}, [globalNotifications]);
|
||||||
|
|
||||||
const addNotification = useCallback(
|
const addNotification = useCallback(
|
||||||
|
|||||||
@@ -42,11 +42,15 @@
|
|||||||
},
|
},
|
||||||
"types": "./dist/cjs/types/index.d.ts",
|
"types": "./dist/cjs/types/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@monaco-editor/react": "^4.5.1",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
|
"@radix-ui/react-popover": "^1.0.6",
|
||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@refocus/sdk": "workspace:^",
|
"@refocus/sdk": "workspace:^",
|
||||||
"framer-motion": "^10.12.16",
|
"framer-motion": "^10.12.16",
|
||||||
|
"monaco-editor": "^0.39.0",
|
||||||
|
"monaco-themes": "^0.4.4",
|
||||||
"react-icons": "^4.9.0",
|
"react-icons": "^4.9.0",
|
||||||
"react-markdown": "^6.0.3",
|
"react-markdown": "^6.0.3",
|
||||||
"styled-components": "6.0.0-rc.3",
|
"styled-components": "6.0.0-rc.3",
|
||||||
|
|||||||
161
packages/ui/src/base/code-editor/index.tsx
Normal file
161
packages/ui/src/base/code-editor/index.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { Range, editor as monacoEditor } from 'monaco-editor';
|
||||||
|
import MonacoEditor from '@monaco-editor/react';
|
||||||
|
import theme from 'monaco-themes/themes/Dracula.json';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { View } from '../view';
|
||||||
|
import { Typography } from '../../typography';
|
||||||
|
|
||||||
|
type CodeEditorProps = {
|
||||||
|
language?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
className?: string;
|
||||||
|
value?: string;
|
||||||
|
highlight?: string;
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
min-height: 300px;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EditorWrapper = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.with-highlight .view-line span span {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.with-highlight .view-line span span.highlight {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||||
|
language,
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
highlight,
|
||||||
|
readOnly = false,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [editor, setEditor] = useState<monacoEditor.ICodeEditor | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current || !editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
editor.layout();
|
||||||
|
});
|
||||||
|
observer.observe(ref.current);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [editor]);
|
||||||
|
const highlightPositions = useMemo(() => {
|
||||||
|
const items = (highlight || '').split(',').map((item) => {
|
||||||
|
const [positions, name] = item.split(':');
|
||||||
|
const [start, end] = positions.split('-');
|
||||||
|
return {
|
||||||
|
start: parseInt(start, 10),
|
||||||
|
end: parseInt(end, 10),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}, [highlight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor.createDecorationsCollection(
|
||||||
|
highlightPositions?.map(({ start, end }) => ({
|
||||||
|
range: new Range(start, 1, end, 1),
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
inlineClassName: 'highlight',
|
||||||
|
},
|
||||||
|
})) || [],
|
||||||
|
);
|
||||||
|
}, [editor, highlightPositions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Wrapper ref={ref} className={className}>
|
||||||
|
<EditorWrapper>
|
||||||
|
<MonacoEditor
|
||||||
|
className={highlight ? 'with-highlight' : 'without-highlight'}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={(nextValue) => setValue(nextValue || '')}
|
||||||
|
beforeMount={(monaco) => {
|
||||||
|
monaco.editor.defineTheme('theme', theme as any);
|
||||||
|
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
|
||||||
|
{
|
||||||
|
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||||
|
allowNonTsExtensions: true,
|
||||||
|
moduleResolution:
|
||||||
|
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||||
|
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
||||||
|
noEmit: true,
|
||||||
|
jsx: monaco.languages.typescript.JsxEmit.ReactJSX,
|
||||||
|
esModuleInterop: true,
|
||||||
|
typeRoots: ['node_modules/@types'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onMount={async (nextEditor) => {
|
||||||
|
setEditor(nextEditor);
|
||||||
|
}}
|
||||||
|
theme="theme"
|
||||||
|
language={language || 'typescript'}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
options={{
|
||||||
|
readOnly: readOnly,
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
fontFamily: 'Fira Code',
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'hidden',
|
||||||
|
horizontal: 'hidden',
|
||||||
|
},
|
||||||
|
wordWrap: 'on',
|
||||||
|
wrappingIndent: 'indent',
|
||||||
|
tabSize: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EditorWrapper>
|
||||||
|
</Wrapper>
|
||||||
|
|
||||||
|
{highlightPositions.length > 0 && (
|
||||||
|
<View $fr $gap="sm" $p="sm">
|
||||||
|
{highlightPositions?.map(({ start, end, name }) => (
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
as="button"
|
||||||
|
onClick={() =>
|
||||||
|
editor?.revealLinesInCenter(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
monacoEditor.ScrollType.Smooth,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
key={`${start}-${end}`}
|
||||||
|
>
|
||||||
|
{name ? `${name}: [${start}-${end}]` : `[${start}-${end}]`}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CodeEditor };
|
||||||
@@ -16,7 +16,10 @@ const Overlay = styled(DialogPrimitives.Overlay)`
|
|||||||
|
|
||||||
const Portal = styled(DialogPrimitives.Portal)``;
|
const Portal = styled(DialogPrimitives.Portal)``;
|
||||||
|
|
||||||
const Content = styled(DialogPrimitives.Content)`
|
const Content = styled(DialogPrimitives.Content)<{
|
||||||
|
maxWidth?: string;
|
||||||
|
height?: string;
|
||||||
|
}>`
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: ${({ theme }) => theme.colors.bg.base100};
|
background-color: ${({ theme }) => theme.colors.bg.base100};
|
||||||
@@ -28,9 +31,10 @@ const Content = styled(DialogPrimitives.Content)`
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
max-width: 450px;
|
max-width: ${({ maxWidth }) => maxWidth || '600px'};
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
|
height: ${({ height }) => height || 'auto'};
|
||||||
box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight};
|
box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ export * from './list';
|
|||||||
export * from './dropdown';
|
export * from './dropdown';
|
||||||
export * from './button';
|
export * from './button';
|
||||||
export * from './masonry';
|
export * from './masonry';
|
||||||
|
export * from './code-editor';
|
||||||
|
export * from './popover';
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ type Props = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
const maxColumnWidth = 400;
|
const maxColumnWidth = 400;
|
||||||
const gutter = 16;
|
const gutter = 16;
|
||||||
|
|
||||||
@@ -104,7 +108,7 @@ const Masonry = ({ children }: Props) => {
|
|||||||
const debouncedHeights = useDebounce(heights, 10);
|
const debouncedHeights = useDebounce(heights, 10);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<Wrapper ref={ref}>
|
||||||
{columnWidth > 0 &&
|
{columnWidth > 0 &&
|
||||||
elements.map((element, index) => (
|
elements.map((element, index) => (
|
||||||
<ItemWrapper
|
<ItemWrapper
|
||||||
@@ -122,7 +126,7 @@ const Masonry = ({ children }: Props) => {
|
|||||||
{element}
|
{element}
|
||||||
</ItemWrapper>
|
</ItemWrapper>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
52
packages/ui/src/base/popover/index.tsx
Normal file
52
packages/ui/src/base/popover/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import * as PopoverPrimitives from '@radix-ui/react-popover';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { styled, css } from 'styled-components';
|
||||||
|
|
||||||
|
const content = css`
|
||||||
|
min-width: 220px;
|
||||||
|
background: ${({ theme }) => theme.colors.bg.base100};
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px;
|
||||||
|
box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35),
|
||||||
|
0px 10px 20px -15px rgba(22, 23, 24, 0.2);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Root = styled(PopoverPrimitives.Root)``;
|
||||||
|
|
||||||
|
const Content = styled(PopoverPrimitives.Content)`
|
||||||
|
${content}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Trigger = styled(PopoverPrimitives.Trigger)`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Portal = styled(PopoverPrimitives.Portal)``;
|
||||||
|
|
||||||
|
const OverlayComponent = styled(motion.div)`
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Overlay: React.FC = () => (
|
||||||
|
<OverlayComponent initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const Arrow = styled(PopoverPrimitives.Arrow)`
|
||||||
|
fill: ${({ theme }) => theme.colors.bg.base100};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Popover = Object.assign(Root, {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
Portal,
|
||||||
|
Overlay,
|
||||||
|
Arrow,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Popover };
|
||||||
@@ -4,15 +4,35 @@ import {
|
|||||||
useRemoveBoard,
|
useRemoveBoard,
|
||||||
useSelectBoard,
|
useSelectBoard,
|
||||||
useSelectedBoard,
|
useSelectedBoard,
|
||||||
|
useSetBoardName,
|
||||||
} from '@refocus/sdk';
|
} from '@refocus/sdk';
|
||||||
import { IoAddCircleOutline } from 'react-icons/io5';
|
import { IoAddCircleOutline } from 'react-icons/io5';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { View } from '../../base';
|
import { View } from '../../base';
|
||||||
import { Board } from '../board';
|
import { Board } from '../board';
|
||||||
import { Tabs } from '../../base/tabs';
|
import { Tabs } from '../../base/tabs';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
const NotificationBar = styled(View)``;
|
const Wrapper = styled(View)`
|
||||||
|
height: 100vh;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title: React.FC<{ id: string }> = ({ id }) => {
|
||||||
|
const boards = useBoards();
|
||||||
|
const board = useMemo(() => boards[id], [boards, id]);
|
||||||
|
const setName = useSetBoardName();
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
$fr
|
||||||
|
$items="center"
|
||||||
|
$gap="sm"
|
||||||
|
$u
|
||||||
|
as="input"
|
||||||
|
value={board.name || ''}
|
||||||
|
onChange={(e) => setName(id, e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const boards = useBoards();
|
const boards = useBoards();
|
||||||
@@ -30,13 +50,13 @@ const App: React.FC = () => {
|
|||||||
}, [addBoardAction]);
|
}, [addBoardAction]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<Wrapper $fc>
|
||||||
<View $f={1}>
|
<View $f={1}>
|
||||||
<Tabs value={selected} onValueChange={selectBoard}>
|
<Tabs value={selected} onValueChange={selectBoard}>
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
{Object.entries(boards).map(([id, board]) => (
|
{Object.entries(boards).map(([id, board]) => (
|
||||||
<Tabs.Trigger key={id} value={id}>
|
<Tabs.Trigger key={id} value={id}>
|
||||||
{board.name}
|
<Title id={id} />
|
||||||
<Tabs.Close onClick={() => removeBoard(id)} />
|
<Tabs.Close onClick={() => removeBoard(id)} />
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
))}
|
))}
|
||||||
@@ -58,8 +78,7 @@ const App: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</View>
|
</View>
|
||||||
<NotificationBar></NotificationBar>
|
</Wrapper>
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Masonry, View } from '../../base';
|
|||||||
import { Widget } from '../widget';
|
import { Widget } from '../widget';
|
||||||
import { AddWidgetFromUrl } from '../add-from-url';
|
import { AddWidgetFromUrl } from '../add-from-url';
|
||||||
import { styled } from 'styled-components';
|
import { styled } from 'styled-components';
|
||||||
|
import { CreateWidget } from '../create-widget';
|
||||||
|
|
||||||
type BoardProps = {
|
type BoardProps = {
|
||||||
board: Board;
|
board: Board;
|
||||||
@@ -20,13 +21,17 @@ const ItemWrapper = styled(View)`
|
|||||||
border-radius: ${({ theme }) => theme.radii.md}px;
|
border-radius: ${({ theme }) => theme.radii.md}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Wrapper = styled(View)`
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
const Board: React.FC<BoardProps> = ({ board, id }) => {
|
const Board: React.FC<BoardProps> = ({ board, id }) => {
|
||||||
const setWidgetData = useUpdateWidget();
|
const setWidgetData = useUpdateWidget();
|
||||||
const removeWidget = useRemoveWidget();
|
const removeWidget = useRemoveWidget();
|
||||||
const addWidget = useAddWidget();
|
const addWidget = useAddWidget();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<Wrapper>
|
||||||
<View $p="md">
|
<View $p="md">
|
||||||
<AddWidgetFromUrl onCreate={(type, data) => addWidget(id, type, data)}>
|
<AddWidgetFromUrl onCreate={(type, data) => addWidget(id, type, data)}>
|
||||||
<AddWidgetFromUrl.Trigger>
|
<AddWidgetFromUrl.Trigger>
|
||||||
@@ -36,6 +41,14 @@ const Board: React.FC<BoardProps> = ({ board, id }) => {
|
|||||||
</View>
|
</View>
|
||||||
</AddWidgetFromUrl.Trigger>
|
</AddWidgetFromUrl.Trigger>
|
||||||
</AddWidgetFromUrl>
|
</AddWidgetFromUrl>
|
||||||
|
<CreateWidget onCreate={(type, data) => addWidget(id, type, data)}>
|
||||||
|
<CreateWidget.Trigger>
|
||||||
|
<View $fr $items="center" $p="sm" $gap="sm">
|
||||||
|
<IoAddCircleOutline />
|
||||||
|
Create
|
||||||
|
</View>
|
||||||
|
</CreateWidget.Trigger>
|
||||||
|
</CreateWidget>
|
||||||
</View>
|
</View>
|
||||||
<View $p="md">
|
<View $p="md">
|
||||||
<Masonry>
|
<Masonry>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
import { Dialog } from '../../base';
|
import { Card, Dialog, View } from '../../base';
|
||||||
import {
|
import { WidgetEditor, WidgetProvider, useWidgets } from '@refocus/sdk';
|
||||||
WidgetEditor,
|
import { Typography } from '../../typography';
|
||||||
WidgetProvider,
|
|
||||||
useWidgets,
|
|
||||||
} from '@refocus/sdk';
|
|
||||||
|
|
||||||
type CreateWidgetProps = {
|
type CreateWidgetProps = {
|
||||||
onCreate: (name: string, data: any) => void;
|
onCreate: (name: string, data: any) => void;
|
||||||
@@ -29,7 +26,23 @@ type WidgetSelectorProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const WidgetSelector: React.FC<WidgetSelectorProps> = ({ onSelect }) => {
|
const WidgetSelector: React.FC<WidgetSelectorProps> = ({ onSelect }) => {
|
||||||
|
const [search, setSearch] = useState<string>('');
|
||||||
const widgets = useWidgets();
|
const widgets = useWidgets();
|
||||||
|
const editableWidgets = useMemo(
|
||||||
|
() => widgets.filter((widget) => widget.edit),
|
||||||
|
[widgets],
|
||||||
|
);
|
||||||
|
const searchResults = useMemo(
|
||||||
|
() =>
|
||||||
|
!search
|
||||||
|
? editableWidgets
|
||||||
|
: editableWidgets.filter((widget) =>
|
||||||
|
widget.name
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes(search.toLocaleLowerCase()),
|
||||||
|
),
|
||||||
|
[search, editableWidgets],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
@@ -39,11 +52,33 @@ const WidgetSelector: React.FC<WidgetSelectorProps> = ({ onSelect }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ height: '50vh' }}>
|
||||||
{widgets.map((widget) => (
|
<Typography
|
||||||
<button key={widget.id} onClick={() => handleSelect(widget.id)}>
|
as="input"
|
||||||
{widget.name}
|
placeholder="Search"
|
||||||
</button>
|
value={search}
|
||||||
|
$u
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
{searchResults.map((widget) => (
|
||||||
|
<Card
|
||||||
|
$fr
|
||||||
|
$items="center"
|
||||||
|
$gap="md"
|
||||||
|
$p="md"
|
||||||
|
key={widget.id}
|
||||||
|
onClick={() => handleSelect(widget.id)}
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<Typography variant="header">{widget.icon}</Typography>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Typography variant="title">{widget.name}</Typography>
|
||||||
|
{widget.description && (
|
||||||
|
<Typography variant="body">{widget.description}</Typography>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -51,16 +86,23 @@ const WidgetSelector: React.FC<WidgetSelectorProps> = ({ onSelect }) => {
|
|||||||
|
|
||||||
const Root: React.FC<CreateWidgetProps> = ({ onCreate, children }) => {
|
const Root: React.FC<CreateWidgetProps> = ({ onCreate, children }) => {
|
||||||
const [id, setId] = useState<string>('');
|
const [id, setId] = useState<string>('');
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleSave = useCallback(
|
const handleSave = useCallback(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
onCreate(id, data);
|
onCreate(id, data);
|
||||||
|
setOpen(false);
|
||||||
},
|
},
|
||||||
[id, onCreate],
|
[id, onCreate],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleOpen = useCallback((state: boolean) => {
|
||||||
|
setOpen(state);
|
||||||
|
setId('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={open} onOpenChange={handleOpen}>
|
||||||
{children}
|
{children}
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay />
|
<Dialog.Overlay />
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import { MdKeyboardArrowUp } from 'react-icons/md';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { VscTrash } from 'react-icons/vsc';
|
import { VscTrash } from 'react-icons/vsc';
|
||||||
import { CgMoreO, CgSync } from 'react-icons/cg';
|
import { CgMoreO, CgSync } from 'react-icons/cg';
|
||||||
|
import { GoScreenFull } from 'react-icons/go';
|
||||||
import { Dialog, View } from '../../base';
|
import { Dialog, View } from '../../base';
|
||||||
import { DropdownMenu } from '../../base';
|
import { DropdownMenu } from '../../base';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { Typography } from '../../typography';
|
import { Typography } from '../../typography';
|
||||||
|
import { NotificationView } from './notification';
|
||||||
|
|
||||||
type WidgetProps = {
|
type WidgetProps = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -115,7 +117,22 @@ const Widget: React.FC<WidgetProps> = ({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
<Title />
|
<Title />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
<NotificationView />
|
||||||
<Update />
|
<Update />
|
||||||
|
<Dialog>
|
||||||
|
<Dialog.Trigger>
|
||||||
|
<View $p="sm">
|
||||||
|
<GoScreenFull />
|
||||||
|
</View>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<Dialog.Content maxWidth="90vw" height="90vh">
|
||||||
|
<Dialog.CloseButton />
|
||||||
|
<WidgetView />
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
{hasMenu && (
|
{hasMenu && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
|
|||||||
53
packages/ui/src/interface/widget/notification.tsx
Normal file
53
packages/ui/src/interface/widget/notification.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
useDismissWidgetNotification,
|
||||||
|
useWidgetNotifications,
|
||||||
|
} from '@refocus/sdk';
|
||||||
|
import { IoNotificationsSharp } from 'react-icons/io5';
|
||||||
|
import { Card, Popover, View } from '../../base';
|
||||||
|
import { useTheme } from 'styled-components';
|
||||||
|
|
||||||
|
const NotificationView: React.FC = () => {
|
||||||
|
const notifications = useWidgetNotifications();
|
||||||
|
const dismiss = useDismissWidgetNotification();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Popover>
|
||||||
|
<Popover.Trigger>
|
||||||
|
<View $p="sm">
|
||||||
|
<IoNotificationsSharp
|
||||||
|
color={
|
||||||
|
notifications.length > 0
|
||||||
|
? theme?.colors.bg.highlight
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Portal>
|
||||||
|
<>
|
||||||
|
<Popover.Overlay />
|
||||||
|
<Popover.Content>
|
||||||
|
{notifications.length === 0 && (
|
||||||
|
<Card $p="sm">No notifications</Card>
|
||||||
|
)}
|
||||||
|
{notifications.map((notification) => (
|
||||||
|
<Card
|
||||||
|
key={notification.id}
|
||||||
|
$p="sm"
|
||||||
|
onClick={() => dismiss(notification.id || '')}
|
||||||
|
>
|
||||||
|
{notification.message}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<Popover.Arrow />
|
||||||
|
</Popover.Content>
|
||||||
|
</>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NotificationView };
|
||||||
58
packages/widgets/src/github/file/edit.tsx
Normal file
58
packages/widgets/src/github/file/edit.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { Props } from './schema';
|
||||||
|
|
||||||
|
type EditorProps = {
|
||||||
|
value?: Props;
|
||||||
|
save: (data: Props) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Edit: React.FC<EditorProps> = ({ value, save }) => {
|
||||||
|
const [owner, setOwner] = useState(value?.owner || '');
|
||||||
|
const [repo, setRepo] = useState(value?.repo || '');
|
||||||
|
const [branch, setBranch] = useState(value?.branch || '');
|
||||||
|
const [path, setPath] = useState(value?.path || '');
|
||||||
|
const [highlight, setHighlight] = useState(value?.highlight || '');
|
||||||
|
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
save({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
branch,
|
||||||
|
path,
|
||||||
|
highlight,
|
||||||
|
});
|
||||||
|
}, [owner, repo, branch, path, highlight, save]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
placeholder="Owner"
|
||||||
|
value={owner}
|
||||||
|
onChange={(e) => setOwner(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
placeholder="Repo"
|
||||||
|
value={repo}
|
||||||
|
onChange={(e) => setRepo(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
placeholder="Branch"
|
||||||
|
value={branch}
|
||||||
|
onChange={(e) => setBranch(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
placeholder="Path"
|
||||||
|
value={path}
|
||||||
|
onChange={(e) => setPath(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
placeholder="Highlights"
|
||||||
|
value={highlight}
|
||||||
|
onChange={(e) => setHighlight(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button onClick={handleSave}>Save</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Edit };
|
||||||
29
packages/widgets/src/github/file/index.tsx
Normal file
29
packages/widgets/src/github/file/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Widget } from '@refocus/sdk';
|
||||||
|
import { SiGithub } from 'react-icons/si';
|
||||||
|
import { schema } from './schema';
|
||||||
|
import { Edit } from './edit';
|
||||||
|
import { View } from './view';
|
||||||
|
|
||||||
|
const widget: Widget<typeof schema> = {
|
||||||
|
name: 'Github File',
|
||||||
|
description: 'Display a file from a Github repository',
|
||||||
|
icon: <SiGithub />,
|
||||||
|
id: 'github.file',
|
||||||
|
parseUrl: (url) => {
|
||||||
|
if (url.hostname !== 'github.com') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pathParts = url.pathname.split('/').filter(Boolean);
|
||||||
|
const [owner, repo, type, branch, ...filePathParts] = pathParts.slice(0);
|
||||||
|
const path = filePathParts.join('/');
|
||||||
|
if (type !== 'blob' || !branch || !path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return { owner, repo, branch, path };
|
||||||
|
},
|
||||||
|
schema,
|
||||||
|
component: View,
|
||||||
|
edit: Edit,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
14
packages/widgets/src/github/file/schema.ts
Normal file
14
packages/widgets/src/github/file/schema.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
|
const schema = Type.Object({
|
||||||
|
owner: Type.String(),
|
||||||
|
repo: Type.String(),
|
||||||
|
path: Type.String(),
|
||||||
|
branch: Type.String(),
|
||||||
|
highlight: Type.Optional(Type.String()),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = Static<typeof schema>;
|
||||||
|
|
||||||
|
export type { Props };
|
||||||
|
export { schema };
|
||||||
62
packages/widgets/src/github/file/view.tsx
Normal file
62
packages/widgets/src/github/file/view.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
useAutoUpdate,
|
||||||
|
useGithubQuery,
|
||||||
|
useName,
|
||||||
|
withGithub,
|
||||||
|
} from '@refocus/sdk';
|
||||||
|
import { Props } from './schema';
|
||||||
|
import { CodeEditor, Github, View } from '@refocus/ui';
|
||||||
|
import { styled } from 'styled-components';
|
||||||
|
|
||||||
|
const FullHeight = styled(View)`
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCodeEditor = styled(CodeEditor)`
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WidgetView = withGithub<Props>(
|
||||||
|
({ owner, repo, branch, path, highlight }) => {
|
||||||
|
const [, setName] = useName();
|
||||||
|
const { data, fetch } = useGithubQuery(async (client, params: Props) => {
|
||||||
|
const response = await client.rest.repos.getContent({
|
||||||
|
owner: params.owner,
|
||||||
|
repo: params.repo,
|
||||||
|
path: params.path,
|
||||||
|
});
|
||||||
|
setName(`${params.owner}/${params.repo}/${params.path}}`);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
useAutoUpdate(
|
||||||
|
{
|
||||||
|
interval: 1000 * 60 * 5,
|
||||||
|
action: async () => fetch({ owner, repo, branch, path }),
|
||||||
|
},
|
||||||
|
[owner, repo, branch, path],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || !('type' in data) || data.type !== 'file') {
|
||||||
|
return <div>Not a file</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FullHeight $fc>
|
||||||
|
<StyledCodeEditor
|
||||||
|
highlight={highlight}
|
||||||
|
value={atob(data.content)}
|
||||||
|
setValue={() => {}}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</FullHeight>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Github.NotLoggedIn,
|
||||||
|
);
|
||||||
|
|
||||||
|
export { WidgetView as View };
|
||||||
@@ -4,6 +4,7 @@ import pullRequest from './pull-request/index.widget';
|
|||||||
import pullRequstComments from './pull-request-comments/index.widget';
|
import pullRequstComments from './pull-request-comments/index.widget';
|
||||||
import workflowRun from './workflow-run/index.widget';
|
import workflowRun from './workflow-run/index.widget';
|
||||||
import workflowRuns from './workflow-runs/index.widget';
|
import workflowRuns from './workflow-runs/index.widget';
|
||||||
|
import file from './file';
|
||||||
|
|
||||||
const github = [
|
const github = [
|
||||||
githubProfileWidget,
|
githubProfileWidget,
|
||||||
@@ -11,6 +12,7 @@ const github = [
|
|||||||
pullRequstComments,
|
pullRequstComments,
|
||||||
workflowRun,
|
workflowRun,
|
||||||
workflowRuns,
|
workflowRuns,
|
||||||
|
file,
|
||||||
] satisfies Widget<any>[];
|
] satisfies Widget<any>[];
|
||||||
|
|
||||||
export { github };
|
export { github };
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { Widget } from '@refocus/sdk';
|
|||||||
import { github } from './github';
|
import { github } from './github';
|
||||||
import { linear } from './linear';
|
import { linear } from './linear';
|
||||||
import { slack } from './slack';
|
import { slack } from './slack';
|
||||||
|
import markdown from './markdown';
|
||||||
|
|
||||||
const widgets = [...linear, ...github, ...slack] satisfies Widget<any>[];
|
const widgets = [
|
||||||
|
...linear,
|
||||||
|
...github,
|
||||||
|
...slack,
|
||||||
|
markdown,
|
||||||
|
] satisfies Widget<any>[];
|
||||||
|
|
||||||
export { widgets };
|
export { widgets };
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ const WidgetView = withLinear<LinearIssueProps>(({ id }) => {
|
|||||||
const issue = await client.issue(id);
|
const issue = await client.issue(id);
|
||||||
const assignee = await issue.assignee;
|
const assignee = await issue.assignee;
|
||||||
const creator = await issue.creator;
|
const creator = await issue.creator;
|
||||||
|
const state = await issue.state;
|
||||||
return {
|
return {
|
||||||
issue,
|
issue,
|
||||||
assignee,
|
assignee,
|
||||||
creator,
|
creator,
|
||||||
|
state,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,6 +36,9 @@ const WidgetView = withLinear<LinearIssueProps>(({ id }) => {
|
|||||||
onClick={() => window.open(data?.issue.url, '_blank')}
|
onClick={() => window.open(data?.issue.url, '_blank')}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
|
<Typography variant="tiny">
|
||||||
|
{data?.state?.name} - {data?.issue.priorityLabel}
|
||||||
|
</Typography>
|
||||||
<Typography variant="title">{data?.issue?.title}</Typography>
|
<Typography variant="title">{data?.issue?.title}</Typography>
|
||||||
<Typography variant="tiny">
|
<Typography variant="tiny">
|
||||||
{data?.issue.description?.substring(0, 100)}
|
{data?.issue.description?.substring(0, 100)}
|
||||||
|
|||||||
36
packages/widgets/src/markdown/edit.tsx
Normal file
36
packages/widgets/src/markdown/edit.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { CodeEditor, Typography } from '@refocus/ui';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { Props } from './schema';
|
||||||
|
|
||||||
|
type EditorProps = {
|
||||||
|
value?: Props;
|
||||||
|
save: (data: Props) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Edit: React.FC<EditorProps> = ({ value, save }) => {
|
||||||
|
const [markdown, setMarkdown] = useState(value?.markdown || '');
|
||||||
|
const [name, setName] = useState(value?.name || '');
|
||||||
|
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
save({
|
||||||
|
name,
|
||||||
|
markdown,
|
||||||
|
});
|
||||||
|
}, [markdown, save, name]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
as="input"
|
||||||
|
$u
|
||||||
|
placeholder="Name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<CodeEditor language="markdown" value={markdown} setValue={setMarkdown} />
|
||||||
|
<button onClick={handleSave}>Save</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Edit };
|
||||||
17
packages/widgets/src/markdown/index.tsx
Normal file
17
packages/widgets/src/markdown/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Widget } from '@refocus/sdk';
|
||||||
|
import { IoLogoMarkdown } from 'react-icons/io';
|
||||||
|
import { schema } from './schema';
|
||||||
|
import { Edit } from './edit';
|
||||||
|
import { View } from './view';
|
||||||
|
|
||||||
|
const widget: Widget<typeof schema> = {
|
||||||
|
name: 'Markdown',
|
||||||
|
description: 'A markdown note',
|
||||||
|
icon: <IoLogoMarkdown />,
|
||||||
|
id: 'text.markdown',
|
||||||
|
schema,
|
||||||
|
component: View,
|
||||||
|
edit: Edit,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
11
packages/widgets/src/markdown/schema.ts
Normal file
11
packages/widgets/src/markdown/schema.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
|
const schema = Type.Object({
|
||||||
|
name: Type.String(),
|
||||||
|
markdown: Type.String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Props = Static<typeof schema>;
|
||||||
|
|
||||||
|
export type { Props };
|
||||||
|
export { schema };
|
||||||
21
packages/widgets/src/markdown/view.tsx
Normal file
21
packages/widgets/src/markdown/view.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useName } from '@refocus/sdk';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { Props } from './schema';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { View } from '@refocus/ui';
|
||||||
|
|
||||||
|
const WidgetView: React.FC<Props> = ({ markdown, name }) => {
|
||||||
|
const [, setName] = useName();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setName(name);
|
||||||
|
}, [name, setName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View $px="md">
|
||||||
|
<ReactMarkdown>{markdown}</ReactMarkdown>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WidgetView as View };
|
||||||
86
pnpm-lock.yaml
generated
86
pnpm-lock.yaml
generated
@@ -147,12 +147,18 @@ importers:
|
|||||||
|
|
||||||
packages/ui:
|
packages/ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@monaco-editor/react':
|
||||||
|
specifier: ^4.5.1
|
||||||
|
version: 4.5.1(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.0.4
|
specifier: ^1.0.4
|
||||||
version: 1.0.4(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.4(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-dropdown-menu':
|
'@radix-ui/react-dropdown-menu':
|
||||||
specifier: ^2.0.5
|
specifier: ^2.0.5
|
||||||
version: 2.0.5(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
version: 2.0.5(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-popover':
|
||||||
|
specifier: ^1.0.6
|
||||||
|
version: 1.0.6(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-tabs':
|
'@radix-ui/react-tabs':
|
||||||
specifier: ^1.0.4
|
specifier: ^1.0.4
|
||||||
version: 1.0.4(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.4(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -162,6 +168,12 @@ importers:
|
|||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^10.12.16
|
specifier: ^10.12.16
|
||||||
version: 10.12.16(react-dom@18.2.0)(react@18.2.0)
|
version: 10.12.16(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
monaco-editor:
|
||||||
|
specifier: ^0.39.0
|
||||||
|
version: 0.39.0
|
||||||
|
monaco-themes:
|
||||||
|
specifier: ^0.4.4
|
||||||
|
version: 0.4.4
|
||||||
react-icons:
|
react-icons:
|
||||||
specifier: ^4.9.0
|
specifier: ^4.9.0
|
||||||
version: 4.9.0(react@18.2.0)
|
version: 4.9.0(react@18.2.0)
|
||||||
@@ -3057,6 +3069,28 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@monaco-editor/loader@1.3.3(monaco-editor@0.39.0):
|
||||||
|
resolution: {integrity: sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==}
|
||||||
|
peerDependencies:
|
||||||
|
monaco-editor: '>= 0.21.0 < 1'
|
||||||
|
dependencies:
|
||||||
|
monaco-editor: 0.39.0
|
||||||
|
state-local: 1.0.7
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@monaco-editor/react@4.5.1(monaco-editor@0.39.0)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-NNDFdP+2HojtNhCkRfE6/D6ro6pBNihaOzMbGK84lNWzRu+CfBjwzGt4jmnqimLuqp5yE5viHS2vi+QOAnD5FQ==}
|
||||||
|
peerDependencies:
|
||||||
|
monaco-editor: '>= 0.25.0 < 1'
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@monaco-editor/loader': 1.3.3(monaco-editor@0.39.0)
|
||||||
|
monaco-editor: 0.39.0
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@ndelangen/get-tarball@3.0.9:
|
/@ndelangen/get-tarball@3.0.9:
|
||||||
resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==}
|
resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3618,6 +3652,40 @@ packages:
|
|||||||
react-remove-scroll: 2.5.5(@types/react@18.0.37)(react@18.2.0)
|
react-remove-scroll: 2.5.5(@types/react@18.0.37)(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-popover@1.0.6(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.22.5
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.0.37)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.0.37)(react@18.2.0)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.0.4(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.0.37)(react@18.2.0)
|
||||||
|
'@radix-ui/react-focus-scope': 1.0.3(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-id': 1.0.1(@types/react@18.0.37)(react@18.2.0)
|
||||||
|
'@radix-ui/react-popper': 1.1.2(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-portal': 1.0.3(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-presence': 1.0.1(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-slot': 1.0.2(@types/react@18.0.37)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.0.37)(react@18.2.0)
|
||||||
|
'@types/react': 18.0.37
|
||||||
|
aria-hidden: 1.2.3
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
react-remove-scroll: 2.5.5(@types/react@18.0.37)(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-popper@1.1.2(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-popper@1.1.2(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==}
|
resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -7032,6 +7100,10 @@ packages:
|
|||||||
/fast-levenshtein@2.0.6:
|
/fast-levenshtein@2.0.6:
|
||||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||||
|
|
||||||
|
/fast-plist@0.1.3:
|
||||||
|
resolution: {integrity: sha512-d9cEfo/WcOezgPLAC/8t8wGb6YOD6JTCPMw2QcG2nAdFmyY+9rTUizCTaGjIZAloWENTEUMAPpkUAIJJJ0i96A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/fastq@1.15.0:
|
/fastq@1.15.0:
|
||||||
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
|
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8580,6 +8652,16 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/monaco-editor@0.39.0:
|
||||||
|
resolution: {integrity: sha512-zhbZ2Nx93tLR8aJmL2zI1mhJpsl87HMebNBM6R8z4pLfs8pj604pIVIVwyF1TivcfNtIPpMXL+nb3DsBmE/x6Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/monaco-themes@0.4.4:
|
||||||
|
resolution: {integrity: sha512-Hbb9pvRrpSi0rZezcB/IOdQnpx10o55Lx4zFdRAAVpFMa1HP7FgaqEZdKffb4ovd90fETCixeFO9JPYFMAq+TQ==}
|
||||||
|
dependencies:
|
||||||
|
fast-plist: 0.1.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mri@1.2.0:
|
/mri@1.2.0:
|
||||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -9899,6 +9981,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/state-local@1.0.7:
|
||||||
|
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/statuses@2.0.1:
|
/statuses@2.0.1:
|
||||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|||||||
Reference in New Issue
Block a user