diff --git a/packages/sdk/src/boards/context.tsx b/packages/sdk/src/boards/context.tsx index 67642df..ebfe5da 100644 --- a/packages/sdk/src/boards/context.tsx +++ b/packages/sdk/src/boards/context.tsx @@ -12,6 +12,7 @@ type BoardsContextValue = { selected?: string; boards: Boards; addBoard: (name: string) => void; + setName: (id: string, name: string) => void; selectBoard: (id: string) => void; removeBoard: (id: string) => void; addWidget: (boardId: string, type: string, data: string) => void; @@ -46,6 +47,16 @@ const BoardsProvider: React.FC = ({ })); }, []); + const setName = useCallback((id: string, name: string) => { + setBoards((currentBoards) => ({ + ...currentBoards, + [id]: { + ...currentBoards[id], + name, + }, + })); + }, []); + const removeBoard = useCallback((id: string) => { setBoards((currentBoards) => { const copy = { ...currentBoards }; @@ -116,6 +127,7 @@ const BoardsProvider: React.FC = ({ addBoard, removeBoard, addWidget, + setName, removeWidget, selectBoard, updateWidget, @@ -126,6 +138,7 @@ const BoardsProvider: React.FC = ({ addBoard, removeBoard, addWidget, + setName, removeWidget, selectBoard, updateWidget, diff --git a/packages/sdk/src/boards/hooks.ts b/packages/sdk/src/boards/hooks.ts index caca25c..3e1d8c3 100644 --- a/packages/sdk/src/boards/hooks.ts +++ b/packages/sdk/src/boards/hooks.ts @@ -65,6 +65,14 @@ const useUpdateWidget = () => { 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 { useBoards, useSelectedBoard, @@ -74,4 +82,5 @@ export { useRemoveBoard, useSelectBoard, useUpdateWidget, + useSetBoardName, }; diff --git a/packages/sdk/src/boards/index.ts b/packages/sdk/src/boards/index.ts index 090ff24..7410f86 100644 --- a/packages/sdk/src/boards/index.ts +++ b/packages/sdk/src/boards/index.ts @@ -8,5 +8,6 @@ export { useRemoveBoard, useSelectBoard, useUpdateWidget, + useSetBoardName, } from './hooks'; export * from './types'; diff --git a/packages/sdk/src/widgets/widget-context.tsx b/packages/sdk/src/widgets/widget-context.tsx index 74dc546..646f1eb 100644 --- a/packages/sdk/src/widgets/widget-context.tsx +++ b/packages/sdk/src/widgets/widget-context.tsx @@ -47,7 +47,7 @@ const WidgetProvider = ({ const addGlobalNotification = useNotificationAdd(); const dissmissGlobalNotification = useNotificationDismiss(); const notifications = useMemo(() => { - return globalNotifications.filter((n) => n.view !== ref.current); + return globalNotifications.filter((n) => n.view === ref.current); }, [globalNotifications]); const addNotification = useCallback( diff --git a/packages/ui/package.json b/packages/ui/package.json index c9ebc58..d7eb32a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -42,11 +42,15 @@ }, "types": "./dist/cjs/types/index.d.ts", "dependencies": { + "@monaco-editor/react": "^4.5.1", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", + "@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-tabs": "^1.0.4", "@refocus/sdk": "workspace:^", "framer-motion": "^10.12.16", + "monaco-editor": "^0.39.0", + "monaco-themes": "^0.4.4", "react-icons": "^4.9.0", "react-markdown": "^6.0.3", "styled-components": "6.0.0-rc.3", diff --git a/packages/ui/src/base/code-editor/index.tsx b/packages/ui/src/base/code-editor/index.tsx new file mode 100644 index 0000000..c47ceb1 --- /dev/null +++ b/packages/ui/src/base/code-editor/index.tsx @@ -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 = ({ + language, + value, + setValue, + highlight, + readOnly = false, + className, +}) => { + const ref = useRef(null); + const [editor, setEditor] = useState(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 ( + <> + + + 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, + }} + /> + + + + {highlightPositions.length > 0 && ( + + {highlightPositions?.map(({ start, end, name }) => ( + + editor?.revealLinesInCenter( + start, + end, + monacoEditor.ScrollType.Smooth, + ) + } + key={`${start}-${end}`} + > + {name ? `${name}: [${start}-${end}]` : `[${start}-${end}]`} + + ))} + + )} + + ); +}; + +export { CodeEditor }; diff --git a/packages/ui/src/base/dialog/index.tsx b/packages/ui/src/base/dialog/index.tsx index 163e977..9232d8a 100644 --- a/packages/ui/src/base/dialog/index.tsx +++ b/packages/ui/src/base/dialog/index.tsx @@ -16,7 +16,10 @@ const Overlay = styled(DialogPrimitives.Overlay)` const Portal = styled(DialogPrimitives.Portal)``; -const Content = styled(DialogPrimitives.Content)` +const Content = styled(DialogPrimitives.Content)<{ + maxWidth?: string; + height?: string; +}>` z-index: 1000; overflow-y: auto; background-color: ${({ theme }) => theme.colors.bg.base100}; @@ -28,9 +31,10 @@ const Content = styled(DialogPrimitives.Content)` left: 50%; transform: translate(-50%, -50%); width: 90vw; - max-width: 450px; + max-width: ${({ maxWidth }) => maxWidth || '600px'}; max-height: 85vh; padding: 25px; + height: ${({ height }) => height || 'auto'}; box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight}; `; diff --git a/packages/ui/src/base/index.ts b/packages/ui/src/base/index.ts index 08be80a..2b6279e 100644 --- a/packages/ui/src/base/index.ts +++ b/packages/ui/src/base/index.ts @@ -8,3 +8,5 @@ export * from './list'; export * from './dropdown'; export * from './button'; export * from './masonry'; +export * from './code-editor'; +export * from './popover'; diff --git a/packages/ui/src/base/masonry/index.tsx b/packages/ui/src/base/masonry/index.tsx index 2ac8c25..64fc07b 100644 --- a/packages/ui/src/base/masonry/index.tsx +++ b/packages/ui/src/base/masonry/index.tsx @@ -15,6 +15,10 @@ type Props = { children: React.ReactNode; }; +const Wrapper = styled.div` + position: relative; +`; + const maxColumnWidth = 400; const gutter = 16; @@ -104,7 +108,7 @@ const Masonry = ({ children }: Props) => { const debouncedHeights = useDebounce(heights, 10); return ( -
+ {columnWidth > 0 && elements.map((element, index) => ( { {element} ))} -
+ ); }; diff --git a/packages/ui/src/base/popover/index.tsx b/packages/ui/src/base/popover/index.tsx new file mode 100644 index 0000000..4dd9970 --- /dev/null +++ b/packages/ui/src/base/popover/index.tsx @@ -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 = () => ( + +); + +const Arrow = styled(PopoverPrimitives.Arrow)` + fill: ${({ theme }) => theme.colors.bg.base100}; +`; + +const Popover = Object.assign(Root, { + Root, + Content, + Trigger, + Portal, + Overlay, + Arrow, +}); + +export { Popover }; diff --git a/packages/ui/src/interface/app/index.tsx b/packages/ui/src/interface/app/index.tsx index 659eb39..b12e206 100644 --- a/packages/ui/src/interface/app/index.tsx +++ b/packages/ui/src/interface/app/index.tsx @@ -4,15 +4,35 @@ import { useRemoveBoard, useSelectBoard, useSelectedBoard, + useSetBoardName, } from '@refocus/sdk'; import { IoAddCircleOutline } from 'react-icons/io5'; import styled from 'styled-components'; import { View } from '../../base'; import { Board } from '../board'; 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 ( + setName(id, e.target.value)} + /> + ); +}; const App: React.FC = () => { const boards = useBoards(); @@ -30,13 +50,13 @@ const App: React.FC = () => { }, [addBoardAction]); return ( - + {Object.entries(boards).map(([id, board]) => ( - {board.name} + <Tabs.Close onClick={() => removeBoard(id)} /> </Tabs.Trigger> ))} @@ -58,8 +78,7 @@ const App: React.FC = () => { ))} </Tabs> </View> - <NotificationBar></NotificationBar> - </View> + </Wrapper> ); }; diff --git a/packages/ui/src/interface/board/index.tsx b/packages/ui/src/interface/board/index.tsx index 3e82c49..a24742a 100644 --- a/packages/ui/src/interface/board/index.tsx +++ b/packages/ui/src/interface/board/index.tsx @@ -9,6 +9,7 @@ import { Masonry, View } from '../../base'; import { Widget } from '../widget'; import { AddWidgetFromUrl } from '../add-from-url'; import { styled } from 'styled-components'; +import { CreateWidget } from '../create-widget'; type BoardProps = { board: Board; @@ -20,13 +21,17 @@ const ItemWrapper = styled(View)` border-radius: ${({ theme }) => theme.radii.md}px; `; +const Wrapper = styled(View)` + height: 100%; +`; + const Board: React.FC<BoardProps> = ({ board, id }) => { const setWidgetData = useUpdateWidget(); const removeWidget = useRemoveWidget(); const addWidget = useAddWidget(); return ( - <View> + <Wrapper> <View $p="md"> <AddWidgetFromUrl onCreate={(type, data) => addWidget(id, type, data)}> <AddWidgetFromUrl.Trigger> @@ -36,6 +41,14 @@ const Board: React.FC<BoardProps> = ({ board, id }) => { </View> </AddWidgetFromUrl.Trigger> </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 $p="md"> <Masonry> diff --git a/packages/ui/src/interface/create-widget/index.tsx b/packages/ui/src/interface/create-widget/index.tsx index e34c27d..d72117d 100644 --- a/packages/ui/src/interface/create-widget/index.tsx +++ b/packages/ui/src/interface/create-widget/index.tsx @@ -1,10 +1,7 @@ -import { useState, useCallback } from 'react'; -import { Dialog } from '../../base'; -import { - WidgetEditor, - WidgetProvider, - useWidgets, -} from '@refocus/sdk'; +import { useState, useCallback, useMemo } from 'react'; +import { Card, Dialog, View } from '../../base'; +import { WidgetEditor, WidgetProvider, useWidgets } from '@refocus/sdk'; +import { Typography } from '../../typography'; type CreateWidgetProps = { onCreate: (name: string, data: any) => void; @@ -29,7 +26,23 @@ type WidgetSelectorProps = { }; const WidgetSelector: React.FC<WidgetSelectorProps> = ({ onSelect }) => { + const [search, setSearch] = useState<string>(''); 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( (id: string) => { @@ -39,11 +52,33 @@ const WidgetSelector: React.FC<WidgetSelectorProps> = ({ onSelect }) => { ); return ( - <div> - {widgets.map((widget) => ( - <button key={widget.id} onClick={() => handleSelect(widget.id)}> - {widget.name} - </button> + <div style={{ height: '50vh' }}> + <Typography + as="input" + placeholder="Search" + 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> ); @@ -51,16 +86,23 @@ const WidgetSelector: React.FC<WidgetSelectorProps> = ({ onSelect }) => { const Root: React.FC<CreateWidgetProps> = ({ onCreate, children }) => { const [id, setId] = useState<string>(''); + const [open, setOpen] = useState<boolean>(false); const handleSave = useCallback( (data: any) => { onCreate(id, data); + setOpen(false); }, [id, onCreate], ); + const handleOpen = useCallback((state: boolean) => { + setOpen(state); + setId(''); + }, []); + return ( - <Dialog> + <Dialog open={open} onOpenChange={handleOpen}> {children} <Dialog.Portal> <Dialog.Overlay /> diff --git a/packages/ui/src/interface/widget/index.tsx b/packages/ui/src/interface/widget/index.tsx index df1a76e..982fd25 100644 --- a/packages/ui/src/interface/widget/index.tsx +++ b/packages/ui/src/interface/widget/index.tsx @@ -13,10 +13,12 @@ import { MdKeyboardArrowUp } from 'react-icons/md'; import { motion } from 'framer-motion'; import { VscTrash } from 'react-icons/vsc'; import { CgMoreO, CgSync } from 'react-icons/cg'; +import { GoScreenFull } from 'react-icons/go'; import { Dialog, View } from '../../base'; import { DropdownMenu } from '../../base'; import { useCallback, useMemo, useState } from 'react'; import { Typography } from '../../typography'; +import { NotificationView } from './notification'; type WidgetProps = { id: string; @@ -115,7 +117,22 @@ const Widget: React.FC<WidgetProps> = ({ </motion.div> <Title /> <Spacer /> + <NotificationView /> <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 && ( <DropdownMenu> <DropdownMenu.Trigger> diff --git a/packages/ui/src/interface/widget/notification.tsx b/packages/ui/src/interface/widget/notification.tsx new file mode 100644 index 0000000..4ac7392 --- /dev/null +++ b/packages/ui/src/interface/widget/notification.tsx @@ -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 }; diff --git a/packages/widgets/src/github/file/edit.tsx b/packages/widgets/src/github/file/edit.tsx new file mode 100644 index 0000000..f9ba158 --- /dev/null +++ b/packages/widgets/src/github/file/edit.tsx @@ -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 }; diff --git a/packages/widgets/src/github/file/index.tsx b/packages/widgets/src/github/file/index.tsx new file mode 100644 index 0000000..f8a0761 --- /dev/null +++ b/packages/widgets/src/github/file/index.tsx @@ -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; diff --git a/packages/widgets/src/github/file/schema.ts b/packages/widgets/src/github/file/schema.ts new file mode 100644 index 0000000..4bfb6d7 --- /dev/null +++ b/packages/widgets/src/github/file/schema.ts @@ -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 }; diff --git a/packages/widgets/src/github/file/view.tsx b/packages/widgets/src/github/file/view.tsx new file mode 100644 index 0000000..b49d6c6 --- /dev/null +++ b/packages/widgets/src/github/file/view.tsx @@ -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 }; diff --git a/packages/widgets/src/github/index.ts b/packages/widgets/src/github/index.ts index 68fefb9..91daeee 100644 --- a/packages/widgets/src/github/index.ts +++ b/packages/widgets/src/github/index.ts @@ -4,6 +4,7 @@ import pullRequest from './pull-request/index.widget'; import pullRequstComments from './pull-request-comments/index.widget'; import workflowRun from './workflow-run/index.widget'; import workflowRuns from './workflow-runs/index.widget'; +import file from './file'; const github = [ githubProfileWidget, @@ -11,6 +12,7 @@ const github = [ pullRequstComments, workflowRun, workflowRuns, + file, ] satisfies Widget<any>[]; export { github }; diff --git a/packages/widgets/src/index.ts b/packages/widgets/src/index.ts index 56ceef4..bc5e652 100644 --- a/packages/widgets/src/index.ts +++ b/packages/widgets/src/index.ts @@ -2,7 +2,13 @@ import { Widget } from '@refocus/sdk'; import { github } from './github'; import { linear } from './linear'; 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 }; diff --git a/packages/widgets/src/linear/issue/view.tsx b/packages/widgets/src/linear/issue/view.tsx index 9ff1b8a..cb69462 100644 --- a/packages/widgets/src/linear/issue/view.tsx +++ b/packages/widgets/src/linear/issue/view.tsx @@ -10,10 +10,12 @@ const WidgetView = withLinear<LinearIssueProps>(({ id }) => { const issue = await client.issue(id); const assignee = await issue.assignee; const creator = await issue.creator; + const state = await issue.state; return { issue, assignee, creator, + state, }; }); @@ -34,6 +36,9 @@ const WidgetView = withLinear<LinearIssueProps>(({ id }) => { onClick={() => window.open(data?.issue.url, '_blank')} > <View> + <Typography variant="tiny"> + {data?.state?.name} - {data?.issue.priorityLabel} + </Typography> <Typography variant="title">{data?.issue?.title}</Typography> <Typography variant="tiny"> {data?.issue.description?.substring(0, 100)} diff --git a/packages/widgets/src/markdown/edit.tsx b/packages/widgets/src/markdown/edit.tsx new file mode 100644 index 0000000..0dbee25 --- /dev/null +++ b/packages/widgets/src/markdown/edit.tsx @@ -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 }; diff --git a/packages/widgets/src/markdown/index.tsx b/packages/widgets/src/markdown/index.tsx new file mode 100644 index 0000000..94fa94f --- /dev/null +++ b/packages/widgets/src/markdown/index.tsx @@ -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; diff --git a/packages/widgets/src/markdown/schema.ts b/packages/widgets/src/markdown/schema.ts new file mode 100644 index 0000000..733caf8 --- /dev/null +++ b/packages/widgets/src/markdown/schema.ts @@ -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 }; diff --git a/packages/widgets/src/markdown/view.tsx b/packages/widgets/src/markdown/view.tsx new file mode 100644 index 0000000..96e1198 --- /dev/null +++ b/packages/widgets/src/markdown/view.tsx @@ -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 }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 835da99..f38362f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,12 +147,18 @@ importers: packages/ui: 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': specifier: ^1.0.4 version: 1.0.4(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dropdown-menu': specifier: ^2.0.5 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': specifier: ^1.0.4 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: specifier: ^10.12.16 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: specifier: ^4.9.0 version: 4.9.0(react@18.2.0) @@ -3057,6 +3069,28 @@ packages: react: 18.2.0 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: resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==} dependencies: @@ -3618,6 +3652,40 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.0.37)(react@18.2.0) 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): resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} peerDependencies: @@ -7032,6 +7100,10 @@ packages: /fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + /fast-plist@0.1.3: + resolution: {integrity: sha512-d9cEfo/WcOezgPLAC/8t8wGb6YOD6JTCPMw2QcG2nAdFmyY+9rTUizCTaGjIZAloWENTEUMAPpkUAIJJJ0i96A==} + dev: false + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: @@ -8580,6 +8652,16 @@ packages: hasBin: 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: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -9899,6 +9981,10 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true + /state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + dev: false + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'}