feat: markdown, github file and UI improvements

This commit is contained in:
Morten Olsen
2023-06-19 16:33:42 +02:00
parent 4ec88717b0
commit 5c23cef034
27 changed files with 767 additions and 26 deletions

View File

@@ -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,

View File

@@ -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,
}; };

View File

@@ -8,5 +8,6 @@ export {
useRemoveBoard, useRemoveBoard,
useSelectBoard, useSelectBoard,
useUpdateWidget, useUpdateWidget,
useSetBoardName,
} from './hooks'; } from './hooks';
export * from './types'; export * from './types';

View File

@@ -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(

View File

@@ -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",

View 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 };

View File

@@ -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};
`; `;

View File

@@ -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';

View File

@@ -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>
); );
}; };

View 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 };

View File

@@ -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>
); );
}; };

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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>

View 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 };

View 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 };

View 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;

View 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 };

View 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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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)}

View 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 };

View 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;

View 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 };

View 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
View File

@@ -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'}