fix: improved UI

This commit is contained in:
Morten Olsen
2023-06-19 09:25:03 +02:00
parent 11299a31fa
commit 85b88822b4
28 changed files with 618 additions and 124 deletions

View File

@@ -11,6 +11,7 @@ const Overlay = styled(DialogPrimitives.Overlay)`
position: fixed;
inset: 0;
backdrop-filter: blur(5px);
background-color: rgba(0, 0, 0, 0.5);
`;
const Portal = styled(DialogPrimitives.Portal)``;
@@ -30,6 +31,7 @@ const Content = styled(DialogPrimitives.Content)`
max-width: 450px;
max-height: 85vh;
padding: 25px;
box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight};
`;
const Title = styled(DialogPrimitives.Title)`

View File

@@ -1,4 +1,5 @@
import * as DropdownMenuPrimitives from '@radix-ui/react-dropdown-menu';
import { motion } from 'framer-motion';
import { styled, css } from 'styled-components';
const RightSlot = styled.div`
@@ -62,6 +63,17 @@ const Trigger = styled(DropdownMenuPrimitives.Trigger)`
const Portal = styled(DropdownMenuPrimitives.Portal)``;
const OverlayComponent = styled(motion.div)`
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
`;
const Overlay: React.FC = () => (
<OverlayComponent initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
);
const Item = styled(DropdownMenuPrimitives.Item)`
${item}
`;
@@ -147,6 +159,7 @@ const DropdownMenu = Object.assign(Root, {
Item,
Sub,
SubTrigger,
Overlay,
SubContent,
Separator,
CheckboxItem,

View File

@@ -1,20 +0,0 @@
import type { StoryFn, Meta } from '@storybook/react';
import { Action } from './index';
import action from './data.json';
const meta = {
title: 'GitHub/Action',
component: Action,
} satisfies Meta<typeof Action>;
type Story = StoryFn<typeof Action>;
const Normal: Story = {
args: {
action: action,
onPress: () => {},
},
} as any;
export { Normal };
export default meta;

View File

@@ -1,56 +0,0 @@
import { useCallback } from 'react';
import { GithubTypes } from '@refocus/sdk';
import {
IoCheckmarkDoneCircleOutline,
IoCloseCircleOutline,
} from 'react-icons/io5';
import { RxTimer } from 'react-icons/rx';
import { FiPlayCircle } from 'react-icons/fi';
import { GoQuestion } from 'react-icons/go';
import { Avatar, Card, View } from '../../base';
import { Typography } from '../../typography';
type ActionProps = {
action: GithubTypes.WorkflowRun;
onPress?: (action: GithubTypes.WorkflowRun) => void;
};
const getIcon = (status: string | null) => {
switch (status) {
case 'success':
return <IoCheckmarkDoneCircleOutline size={48} color="green" />;
case 'failure':
return <IoCloseCircleOutline size={48} color="red" />;
case 'in_progress':
return <FiPlayCircle size={48} />;
case 'queued':
return <RxTimer size={48} />;
default:
return <GoQuestion size={48} />;
}
};
const Action: React.FC<ActionProps> = ({ action, onPress }) => {
const onPressHandler = useCallback(() => {
onPress?.(action);
}, [action, onPress]);
return (
<Card $fr $items="center" $p="md" $gap="md" onClick={onPressHandler}>
<Avatar
url={action.actor?.avatar_url}
name={action.actor?.name || action.actor?.login}
decal={`#${action.run_attempt}`}
/>
<View $fc $f={1}>
<Typography variant="overline">
{action.name} - {action.actor?.name || action.actor?.login}
</Typography>
<Typography variant="title">{action.display_title}</Typography>
<Typography variant="subtitle">{action.status}</Typography>
</View>
<View>{getIcon(action.status)}</View>
</Card>
);
};
export { Action };

View File

@@ -3,3 +3,4 @@ export * from './profile';
export * from './pull-request';
export * from './login';
export * from './not-logged-in';
export * from './workflow-run';

View File

@@ -0,0 +1,20 @@
import type { StoryFn, Meta } from '@storybook/react';
import { WorkflowRun } from './index';
import workflowRun from './data.json';
const meta = {
title: 'GitHub/Workflow Run',
component: WorkflowRun,
} satisfies Meta<typeof WorkflowRun>;
type Story = StoryFn<typeof WorkflowRun>;
const Normal: Story = {
args: {
workflowRun,
onPress: () => {},
},
} as any;
export { Normal };
export default meta;

View File

@@ -0,0 +1,72 @@
import { useCallback } from 'react';
import { GithubTypes } from '@refocus/sdk';
import {
IoCheckmarkDoneCircleOutline,
IoCloseCircleOutline,
} from 'react-icons/io5';
import { RxTimer } from 'react-icons/rx';
import { FiPlayCircle } from 'react-icons/fi';
import { GoQuestion } from 'react-icons/go';
import { Avatar, Card, View } from '../../base';
import { Typography } from '../../typography';
type WorkflowRunProps = {
workflowRun: GithubTypes.WorkflowRun;
onPress?: (action: GithubTypes.WorkflowRun) => void;
};
const getIcon = (workflowRun: GithubTypes.WorkflowRun) => {
const { status, conclusion } = workflowRun;
if (status === 'completed' && conclusion === 'success') {
return <IoCheckmarkDoneCircleOutline size={48} color="green" />;
} else if (status === 'completed' && conclusion === 'failure') {
return <IoCloseCircleOutline size={48} color="red" />;
} else if (status === 'completed' && conclusion === 'cancelled') {
return <IoCloseCircleOutline size={48} color="yellow" />;
} else if (status === 'completed' && conclusion === 'skipped') {
return <IoCloseCircleOutline size={48} color="gray" />;
} else if (status === 'completed' && conclusion === 'timed_out') {
return <IoCloseCircleOutline size={48} color="gray" />;
} else if (status === 'completed' && conclusion === 'action_required') {
return <IoCloseCircleOutline size={48} color="gray" />;
} else if (status === 'in_progress') {
return <FiPlayCircle size={48} />;
} else if (status === 'queued') {
return <RxTimer size={48} />;
} else {
return <GoQuestion size={48} />;
}
};
const WorkflowRun: React.FC<WorkflowRunProps> = ({ workflowRun, onPress }) => {
const onPressHandler = useCallback(() => {
onPress?.(workflowRun);
}, [workflowRun, onPress]);
return (
<Card
$fr
$items="center"
$p="md"
$gap="md"
onClick={onPressHandler}
$m="sm"
>
<Avatar
url={workflowRun.actor?.avatar_url}
name={workflowRun.actor?.name || workflowRun.actor?.login}
decal={`#${workflowRun.run_attempt}`}
/>
<View $fc $f={1}>
<Typography variant="overline">
{workflowRun.repository.full_name} -{' '}
{workflowRun.actor?.name || workflowRun.actor?.login}
</Typography>
<Typography variant="title">{workflowRun.display_title}</Typography>
<Typography variant="subtitle">{workflowRun.name}</Typography>
</View>
<View>{getIcon(workflowRun)}</View>
</Card>
);
};
export { WorkflowRun };

View File

@@ -16,10 +16,7 @@ type BoardProps = {
};
const ItemWrapper = styled(View)`
overflow-y: auto;
max-height: 500px;
max-width: 100%;
box-shadow: 0 0 4px 0px ${({ theme }) => theme.colors.bg.highlight};
border-radius: ${({ theme }) => theme.radii.md}px;
`;

View File

@@ -3,15 +3,20 @@ import {
WidgetEditor,
WidgetProvider,
WidgetView,
useHasUpdate,
useName,
useReloadWidget,
useWidget,
useIsUpdating,
} from '@refocus/sdk';
import { MdKeyboardArrowUp } from 'react-icons/md';
import { motion } from 'framer-motion';
import { VscTrash } from 'react-icons/vsc';
import { CgMoreO } from 'react-icons/cg';
import { CgMoreO, CgSync } from 'react-icons/cg';
import { Dialog, View } from '../../base';
import { DropdownMenu } from '../../base';
import { useCallback, useMemo, useState } from 'react';
import { Typography } from '../../typography';
type WidgetProps = {
id: string;
@@ -23,18 +28,55 @@ type WidgetProps = {
const Wrapper = styled(View)`
background: ${({ theme }) => theme.colors.bg.base};
box-shadow: 0 0 0 1px ${({ theme }) => theme.colors.bg.highlight};
margin: 5px;
border-radius: 5px;
`;
const WidgetWrapper = styled(View)`
flex-grow: 0;
overflow: hidden;
flex: 1;
max-height: 500px;
overflow-y: auto;
`;
const Spacer = styled(View)`
flex: 1;
`;
const SingleLine = styled(Typography)`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const Title: React.FC = () => {
const [name] = useName();
return <SingleLine variant="overline">{name}</SingleLine>;
};
const Update: React.FC = () => {
const hasUpdate = useHasUpdate();
const reload = useReloadWidget();
const updating = useIsUpdating();
if (!hasUpdate) {
return null;
}
return (
<motion.div
animate={{ rotate: updating ? 360 : 0 }}
transition={{ duration: 1, loop: Infinity }}
>
<View $p="sm" onClick={reload}>
<CgSync />
</View>
</motion.div>
);
};
const Widget: React.FC<WidgetProps> = ({
id,
data,
@@ -59,7 +101,7 @@ const Widget: React.FC<WidgetProps> = ({
);
return (
<WidgetProvider id={id} data={data} setData={setData}>
<View $fr>
<View $fr $items="center">
<motion.div animate={{ rotate: open ? 180 : 0 }}>
<View
$items="center"
@@ -71,7 +113,9 @@ const Widget: React.FC<WidgetProps> = ({
<MdKeyboardArrowUp size={22} />
</View>
</motion.div>
<Title />
<Spacer />
<Update />
{hasMenu && (
<DropdownMenu>
<DropdownMenu.Trigger>
@@ -80,27 +124,32 @@ const Widget: React.FC<WidgetProps> = ({
</View>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content alignOffset={50}>
{!!onRemove && (
<DropdownMenu.Item onClick={onRemove}>
<DropdownMenu.Icon>
<VscTrash color={theme?.colors.simple.red} />
</DropdownMenu.Icon>
Remove
</DropdownMenu.Item>
)}
{!!widget?.edit && !!setData && (
<DropdownMenu.Item onClick={() => setShowEdit(true)}>
Edit
</DropdownMenu.Item>
)}
<DropdownMenu.Arrow />
</DropdownMenu.Content>
<>
<DropdownMenu.Overlay />
<DropdownMenu.Content alignOffset={50}>
{!!onRemove && (
<DropdownMenu.Item onClick={onRemove}>
<DropdownMenu.Icon>
<VscTrash color={theme?.colors.simple.red} />
</DropdownMenu.Icon>
Remove
</DropdownMenu.Item>
)}
{!!widget?.edit && !!setData && (
<DropdownMenu.Item onClick={() => setShowEdit(true)}>
Edit
</DropdownMenu.Item>
)}
<DropdownMenu.Arrow />
</DropdownMenu.Content>
</>
</DropdownMenu.Portal>
</DropdownMenu>
)}
</View>
<motion.div animate={{ height: open ? 'auto' : 0 }}>
<motion.div
animate={{ height: open ? 'auto' : 0, opacity: open ? 1 : 0 }}
>
<Wrapper className={className} $fr>
<WidgetWrapper $f={1}>
<WidgetView />
@@ -108,13 +157,15 @@ const Widget: React.FC<WidgetProps> = ({
</Wrapper>
</motion.div>
<Dialog open={showEdit} onOpenChange={setShowEdit}>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Edit Widget</Dialog.Title>
<Dialog.Description>
<WidgetEditor onSave={onSave} />
</Dialog.Description>
</Dialog.Content>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Edit Widget</Dialog.Title>
<Dialog.Description>
<WidgetEditor onSave={onSave} />
</Dialog.Description>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
</WidgetProvider>
);

View File

@@ -6,7 +6,6 @@ type UIProviderProps = {
};
// @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700&display=swap');
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
overflow-wrap: break-word;
@@ -24,6 +23,7 @@ const GlobalStyle = createGlobalStyle`
body {
background-color: ${({ theme }) => theme.colors.bg.base};
background-image: linear-gradient(to right, rgb(38, 39, 54), rgb(25, 26, 35));
color: ${({ theme }) => theme.colors.text.base};
margin: 0;
padding: 0;