mirror of
https://github.com/morten-olsen/refocus.dev.git
synced 2026-02-08 00:46:25 +01:00
fix: improved UI
This commit is contained in:
@@ -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)`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -3,3 +3,4 @@ export * from './profile';
|
||||
export * from './pull-request';
|
||||
export * from './login';
|
||||
export * from './not-logged-in';
|
||||
export * from './workflow-run';
|
||||
|
||||
20
packages/ui/src/github/workflow-run/index.stories.tsx
Normal file
20
packages/ui/src/github/workflow-run/index.stories.tsx
Normal 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;
|
||||
72
packages/ui/src/github/workflow-run/index.tsx
Normal file
72
packages/ui/src/github/workflow-run/index.tsx
Normal 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 };
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user