This commit is contained in:
Morten Olsen
2023-06-16 11:10:50 +02:00
commit bc0d501d98
163 changed files with 16458 additions and 0 deletions

2
packages/ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/node_modules/
/dist/

View File

@@ -0,0 +1,17 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;

View File

@@ -0,0 +1,24 @@
import React from 'react';
import type { Preview } from '@storybook/react';
import { UIProvider } from '../src/theme/provider';
const preview: Preview = {
decorators: [
(Story) => (
<UIProvider>
<Story />
</UIProvider>
),
],
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

53
packages/ui/package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"devDependencies": {
"@refocus/config": "workspace:^",
"@storybook/addon-essentials": "^7.0.20",
"@storybook/addon-interactions": "^7.0.20",
"@storybook/addon-links": "^7.0.20",
"@storybook/blocks": "^7.0.20",
"@storybook/react": "^7.0.20",
"@storybook/react-vite": "^7.0.20",
"@storybook/testing-library": "^0.0.14-next.2",
"@types/react": "^18.0.37",
"@types/styled-components": "^5.1.26",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"storybook": "^7.0.20",
"typescript": "^5.0.4"
},
"exports": {
".": {
"import": {
"default": "./dist/esm/index.js",
"types": "./dist/esm/types/index.d.ts"
},
"require": {
"default": "./dist/cjs/index.js",
"types": "./dist/cjs/types/index.d.ts"
}
}
},
"files": [
"dist/**/*"
],
"main": "./dist/cjs/index.js",
"name": "@refocus/ui",
"scripts": {
"build": "pnpm build:esm && pnpm build:cjs",
"build:cjs": "tsc -p tsconfig.json",
"build:esm": "tsc -p tsconfig.esm.json",
"storybook": "storybook dev -p 6006 --no-open",
"build-storybook": "storybook build"
},
"types": "./dist/cjs/types/index.d.ts",
"dependencies": {
"@refocus/sdk": "workspace:^",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-tabs": "^1.0.4",
"react-icons": "^4.9.0",
"react-markdown": "^6.0.3",
"styled-components": "6.0.0-rc.3"
}
}

View File

@@ -0,0 +1,84 @@
import styled from 'styled-components';
import { useMemo } from 'react';
type AvatarProps = {
url?: string;
name?: string;
decal?: React.ReactNode;
size?: 'sm' | 'md' | 'lg';
};
const sizes = {
sm: 28,
md: 50,
lg: 75,
};
const fontSizes = {
sm: 10,
md: 24,
lg: 32,
};
const Wrapper = styled.div<{
size: AvatarProps['size'];
}>`
position: relative;
flex-shrink: 0;
${({ size }) => (size ? `width: ${sizes[size]}px;` : '')}
${({ size }) => (size ? `height: ${sizes[size]}px;` : '')}
`;
const Image = styled.img`
width: 100%;
height: 100%;
border: 3px solid ${({ theme }) => theme.colors.text.base};
border-radius: 50%;
`;
const WithoutImage = styled.div<{
size: AvatarProps['size'];
}>`
width: 100%;
height: 100%;
border: 3px solid ${({ theme }) => theme.colors.text.base};
border-radius: 50%;
display: flex;
justify-content: center;
font-weight: bold;
align-items: center;
color: ${({ theme }) => theme.colors.text.base};
${({ size }) => (size ? `font-size: ${fontSizes[size]}px;` : '')}
`;
const Decal = styled.div`
position: absolute;
bottom: 0;
right: -5px;
min-width: 20px;
height: 20px;
border-radius: 10px;
padding: 0 5px;
background-color: ${({ theme }) => theme.colors.text.base};
display: flex;
justify-content: center;
align-items: center;
color: ${({ theme }) => theme.colors.bg.base};
font-size: 12px;
`;
const Avatar: React.FC<AvatarProps> = ({ url, name, decal, size = 'md' }) => {
const initials = useMemo(() => {
const [firstName, lastName] = name?.split(' ') || [];
return `${firstName?.[0] || ''}${lastName?.[0] || ''}`;
}, [name]);
return (
<Wrapper size={size}>
{!url && <WithoutImage size={size}>{initials}</WithoutImage>}
{url && <Image src={url} alt={name || ''} />}
{decal && <Decal>{decal}</Decal>}
</Wrapper>
);
};
export { Avatar };

View File

@@ -0,0 +1,30 @@
import { styled } from 'styled-components';
import { View } from '../view';
type ButtonProps = {
title: React.ReactNode;
icon?: React.ReactNode;
onClick?: () => void;
};
const ButtonWrapper = styled(View)`
background-color: ${({ theme }) => theme.colors.bg.highlight};
display: inline-flex;
`;
const Button: React.FC<ButtonProps> = ({ title, onClick, icon }) => {
return (
<ButtonWrapper
$p="sm"
as="button"
onClick={onClick}
$items="center"
$gap="sm"
>
{icon}
{title}
</ButtonWrapper>
);
};
export { Button };

View File

@@ -0,0 +1,24 @@
import styled from 'styled-components';
import { View } from '../view';
const Card = styled(View)`
border-radius: ${({ theme }) => `${theme.radii.md}${theme.units.radii}`};
${({ theme, ...rest }) =>
'onClick' in rest &&
`
cursor: pointer;
transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
&:hover {
box-shadow: 0 0 5px 2px ${theme.colors.bg.highlight};
background: ${theme.colors.bg.highlight100};
}
&:active {
box-shadow: 0 0 3px 2px ${theme.colors.bg.highlight100};
background: ${theme.colors.bg.highlight100};
}
`}
`;
export { Card };

View File

@@ -0,0 +1,30 @@
import { StoryObj, Meta } from '@storybook/react';
import { Dialog } from '.';
type Story = StoryObj<typeof Dialog>;
const meta = {
title: 'Interface/Dialog',
component: Dialog,
} satisfies Meta<typeof Dialog>;
const docs: Story = {
render: () => (
<Dialog>
<Dialog.Trigger asChild>
<button>Open Dialog</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog Description</Dialog.Description>
<Dialog.CloseButton />
</Dialog.Content>
</Dialog.Portal>
</Dialog>
),
};
export default meta;
export { docs };

View File

@@ -0,0 +1,102 @@
import * as DialogPrimitives from '@radix-ui/react-dialog';
import React, { forwardRef } from 'react';
import { styled } from 'styled-components';
import { FiX } from 'react-icons/fi';
import { styles } from '../../typography';
import { View } from '../view';
const Root = styled(DialogPrimitives.Root)``;
const Overlay = styled(DialogPrimitives.Overlay)`
position: fixed;
inset: 0;
backdrop-filter: blur(5px);
`;
const Portal = styled(DialogPrimitives.Portal)``;
const Content = styled(DialogPrimitives.Content)`
z-index: 1000;
overflow-y: auto;
background-color: ${({ theme }) => theme.colors.bg.base100};
border-radius: 6px;
box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90vw;
max-width: 450px;
max-height: 85vh;
padding: 25px;
`;
const Title = styled(DialogPrimitives.Title)`
margin: 0;
font-weight: 500;
font-size: 17px;
${styles.dialogTitle}
`;
const Description = styled(DialogPrimitives.Description)`
margin: 10px 0 20px;
color: var(--mauve11);
font-size: 15px;
line-height: 1.5;
`;
const Trigger = styled(DialogPrimitives.Trigger)``;
const Close = styled(DialogPrimitives.Close)`
all: unset;
`;
const CloseButtonWrapper = styled(DialogPrimitives.Close)`
all: unset;
font-family: inherit;
border-radius: 100%;
height: 25px;
width: 25px;
display: inline-flex;
align-items: center;
justify-content: center;
position: absolute;
top: 10px;
right: 10px;
&:hover {
background-color: var(--violet4);
}
&:focus {
box-shadow: 0 0 0 2px var(--violet7);
}
`;
const Buttons = styled(View)`
display: flex;
justify-content: flex-end;
`;
const CloseButton = forwardRef<
React.ComponentProps<typeof CloseButtonWrapper>,
React.ComponentPropsWithoutRef<typeof CloseButtonWrapper>
>((props, forwardedRef) => (
<CloseButtonWrapper {...props} ref={forwardedRef as any}>
<FiX />
</CloseButtonWrapper>
));
const Dialog = Object.assign(Root, {
Overlay,
Portal,
Content,
Title,
Description,
Trigger,
CloseButton,
Close,
Buttons,
});
export { Dialog };

View File

@@ -0,0 +1,43 @@
import { StoryObj, Meta } from '@storybook/react';
import { DropdownMenu } from '.';
type Story = StoryObj<typeof DropdownMenu>;
const meta = {
title: 'Components/DropDown',
component: DropdownMenu,
} satisfies Meta<typeof DropdownMenu>;
const docs: Story = {
render: () => (
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<button>Open Dialog</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>
Item 1<DropdownMenu.RightSlot>Foo</DropdownMenu.RightSlot>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>
Item 2<DropdownMenu.RightSlot>+A</DropdownMenu.RightSlot>
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent sideOffset={2} alignOffset={-5}>
<DropdownMenu.Item>Sub Item 1</DropdownMenu.Item>
<DropdownMenu.Item>Sub Item 2</DropdownMenu.Item>
<DropdownMenu.Item>Sub Item 3</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
<DropdownMenu.Arrow />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
),
};
export default meta;
export { docs };

View File

@@ -0,0 +1,162 @@
import * as DropdownMenuPrimitives from '@radix-ui/react-dropdown-menu';
import { styled, css } from 'styled-components';
const RightSlot = styled.div`
margin-left: auto;
padding-left: 20px;
color: var(--mauve11);
`;
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 item = css`
font-size: 13px;
line-height: 1;
border-radius: 3px;
display: flex;
align-items: center;
height: 25px;
padding: 0 5px;
position: relative;
padding-left: 25px;
user-select: none;
outline: none;
&[data-state='disabled'] {
color: ${({ theme }) => theme.colors.text.disabled};
pointer-events: none;
}
&[data-highlighted] {
background-color: ${({ theme }) => theme.colors.bg.highlight};
color: ${({ theme }) => theme.colors.text.highlight};
}
&[data-highlighted] > ${RightSlot} {
color: white;
}
&[data-disabled] ${RightSlot} {
color: var(--mauve8);
}
`;
const Root = styled(DropdownMenuPrimitives.Root)``;
const Content = styled(DropdownMenuPrimitives.Content)`
${content}
`;
const Trigger = styled(DropdownMenuPrimitives.Trigger)`
display: flex;
align-items: center;
justify-content: center;
`;
const Portal = styled(DropdownMenuPrimitives.Portal)``;
const Item = styled(DropdownMenuPrimitives.Item)`
${item}
`;
const Sub = styled(DropdownMenuPrimitives.Sub)``;
const SubTrigger = styled(DropdownMenuPrimitives.SubTrigger)`
&[data-state='open'] {
background-color: ${({ theme }) => theme.colors.bg.highlight100};
}
${item}
`;
const Icon = styled.div`
position: absolute;
left: 0;
width: 25px;
display: inline-flex;
align-items: center;
justify-content: center;
`;
const SubContent = styled(DropdownMenuPrimitives.SubContent)`
${content}
min-width: 220px;
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 Separator = styled(DropdownMenuPrimitives.Separator)`
height: 1px;
background-color: ${({ theme }) => theme.colors.bg.highlight100};
margin: 5px;
`;
const CheckboxItem = styled(DropdownMenuPrimitives.CheckboxItem)``;
const ItemIndicator = styled(DropdownMenuPrimitives.ItemIndicator)`
position: absolute;
left: 0;
width: 25px;
display: inline-flex;
align-items: center;
justify-content: center;
`;
const Label = styled(DropdownMenuPrimitives.Label)`
padding-left: 25px;
font-size: 12px;
line-height: 25px;
color: var(--mauve11);
`;
const RadioGroup = styled(DropdownMenuPrimitives.RadioGroup)``;
const RadioItem = styled(DropdownMenuPrimitives.RadioItem)`
font-size: 13px;
line-height: 1;
color: var(--violet11);
border-radius: 3px;
display: flex;
align-items: center;
height: 25px;
padding: 0 5px;
position: relative;
padding-left: 25px;
user-select: none;
outline: none;
`;
const Arrow = styled(DropdownMenuPrimitives.Arrow)`
fill: ${({ theme }) => theme.colors.bg.base100};
`;
const DropdownMenu = Object.assign(Root, {
Root,
Content,
Trigger,
Portal,
Item,
Sub,
SubTrigger,
SubContent,
Separator,
CheckboxItem,
ItemIndicator,
RightSlot,
Label,
RadioGroup,
RadioItem,
Arrow,
Icon,
});
export { DropdownMenu };

View File

@@ -0,0 +1,9 @@
export * from './card';
export * from './panel';
export * from './row';
export * from './view';
export * from './avatar';
export * from './dialog';
export * from './list';
export * from './dropdown';
export * from './button';

View File

@@ -0,0 +1,15 @@
import { View } from '../view';
type ListProps = {
children: React.ReactNode;
};
const List = ({ children }: ListProps) => {
return (
<View $fc $gap="sm" $p="sm">
{children}
</View>
);
};
export { List };

View File

@@ -0,0 +1,11 @@
.root {
@apply bg-white rounded-lg shadow-lg;
}
.title {
@apply text-2xl font-bold;
}
.content {
@apply p-4;
}

View File

@@ -0,0 +1,16 @@
type PanelProps = {
title: string;
children: React.ReactNode;
className?: string;
};
const Panel: React.FC<PanelProps> = ({ title, children, className }) => {
return (
<div className={className}>
<h1>{title}</h1>
<div>{children}</div>
</div>
);
};
export { Panel };

View File

@@ -0,0 +1,13 @@
type RowProps = {
title: string;
};
const Row: React.FC<RowProps> = ({ title }) => {
return (
<div>
<h3 className="text-lg font-bold">{title}</h3>
</div>
);
};
export { Row };

View File

@@ -0,0 +1,68 @@
import * as TabsPrimities from '@radix-ui/react-tabs';
import { FiX } from 'react-icons/fi';
import styled from 'styled-components';
const Root = styled(TabsPrimities.Root)`
display: flex;
flex-direction: column;
height: 100%;
`;
const List = styled(TabsPrimities.List)`
flex-shrink: 0;
display: flex;
border-bottom: 1px solid var(--mauve6);
overflow-x: auto;
border-bottom: 1px solid ${({ theme }) => theme.colors.bg.base100};
`;
const Trigger = styled(TabsPrimities.Trigger)`
font-family: inherit;
padding: 0 20px;
height: 45px;
max-width: 200px;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 15px;
line-height: 1;
user-select: none;
border-right: 1px solid ${({ theme }) => theme.colors.bg.base100};
&:hover {
color: ${({ theme }) => theme.colors.bg.highlight};
}
&[data-state='active'] {
color: ${({ theme }) => theme.colors.bg.highlight};
box-shadow: inset 0 -1px 0 0 currentColor, 0 2px 0 0 currentColor;
}
`;
const Content = styled(TabsPrimities.Content)`
flex-grow: 1;
outline: none;
overflow-y: auto;
`;
const CloseWrapper = styled.div``;
type CloseProps = {
onClick?: () => void;
};
const Close: React.FC<CloseProps> = ({ onClick }) => (
<CloseWrapper onClick={onClick}>
<FiX />
</CloseWrapper>
);
const Tabs = Object.assign(Root, {
List,
Trigger,
Content,
Close,
});
export { Tabs };

View File

@@ -0,0 +1,91 @@
import { styled } from 'styled-components';
import { Theme } from '../../theme';
type SizeKey = keyof Theme['space'];
type BgKey = keyof Theme['colors']['bg'];
type ColorKey = keyof Theme['colors']['text'];
const getSize = (
theme: Theme,
sizes: (SizeKey | undefined)[],
multi: number = 1,
) => {
while (sizes.length) {
const size = sizes.shift();
if (size) {
const value = theme.space[size];
if (value) {
return `${value * multi}${theme.units.space}`;
}
}
}
return '0';
};
const View = styled.div<{
$br?: boolean;
$bg?: BgKey;
$m?: SizeKey;
$mt?: SizeKey;
$mr?: SizeKey;
$mb?: SizeKey;
$ml?: SizeKey;
$mx?: SizeKey;
$my?: SizeKey;
$mm?: number;
$c?: ColorKey;
$p?: SizeKey;
$pt?: SizeKey;
$pr?: SizeKey;
$pb?: SizeKey;
$pl?: SizeKey;
$px?: SizeKey;
$py?: SizeKey;
$pm?: number;
$fr?: boolean;
$fc?: boolean;
$u?: boolean;
$gap?: SizeKey;
$flexWrap?: boolean;
$f?: number;
$items?: 'center' | 'flex-start' | 'flex-end' | 'stretch' | 'baseline';
$justify?:
| 'center'
| 'flex-start'
| 'flex-end'
| 'space-between'
| 'space-around'
| 'space-evenly'
| 'stretch';
}>`
${({ $u }) => $u && 'all: unset;'}
${({ $br }) => $br && 'border-radius: 5px;'}
${({ $gap, theme }) =>
$gap && `gap: ${theme.space[$gap]}${theme.units.space};`}
${({ $c, theme }) => $c && `color: ${theme.colors.text[$c]};`}
${({ $bg, theme }) => $bg && `background-color: ${theme.colors.bg[$bg]};`}
margin-top: ${({ theme, $mt, $my, $m, $mm }) =>
getSize(theme, [$mt, $my, $m], $mm)};
margin-right: ${({ theme, $mr, $mx, $m, $mm }) =>
getSize(theme, [$mr, $mx, $m], $mm)};
margin-bottom: ${({ theme, $mb, $my, $m, $mm }) =>
getSize(theme, [$mb, $my, $m], $mm)};
margin-left: ${({ theme, $ml, $mx, $m, $mm }) =>
getSize(theme, [$ml, $mx, $m], $mm)};
padding-top: ${({ theme, $pt, $py, $p, $pm }) =>
getSize(theme, [$pt, $py, $p], $pm)};
padding-right: ${({ theme, $pr, $px, $p, $pm }) =>
getSize(theme, [$pr, $px, $p], $pm)};
padding-bottom: ${({ theme, $pb, $py, $p, $pm }) =>
getSize(theme, [$pb, $py, $p], $pm)};
padding-left: ${({ theme, $pl, $px, $p, $pm }) =>
getSize(theme, [$pl, $px, $p], $pm)};
${({ $fr }) => $fr && 'display: flex;'}
${({ $fc }) => $fc && 'flex-direction: column; display: flex;'}
${({ $f }) => $f && `flex: ${$f};`}
${({ $flexWrap }) => $flexWrap && 'flex-wrap: wrap;'}
${({ $items }) => $items && `align-items: ${$items};`}
${({ $justify }) => $justify && `justify-content: ${$justify};`}
`;
export { View };

View File

@@ -0,0 +1,35 @@
import { styled } from 'styled-components';
import { View } from '../../base';
type ComposeProps = {
value: string;
onValueChange: (value: string) => void;
onSend?: () => void;
};
const Input = styled(View)`
background: ${({ theme }) => theme.colors.bg.highlight100};
padding: ${({ theme }) => `${theme.space.sm}${theme.units.space}`};
`;
const Send = styled.button`
all: unset;
background: ${({ theme }) => theme.colors.bg.highlight};
padding: ${({ theme }) => `${theme.space.sm}${theme.units.space}`};
`;
const Compose: React.FC<ComposeProps> = ({ value, onValueChange, onSend }) => (
<View $fr>
<Input
$f={1}
$u
placeholder="Type a message..."
as="input"
value={value}
onChange={(e) => onValueChange(e.target.value)}
/>
{!!onSend && <Send onClick={onSend}>Send</Send>}
</View>
);
export { Compose };

View File

@@ -0,0 +1,2 @@
export * from './message';
export * from './compose';

View File

@@ -0,0 +1,26 @@
import type { StoryFn, Meta } from '@storybook/react';
import { Message } from './index';
const meta = {
title: 'Chat/Message',
component: Message,
} satisfies Meta<typeof Message>;
type Story = StoryFn<typeof Message>;
const Normal: Story = {
args: {
message: {
text: 'Hello World',
timestamp: new Date('2023-01-01T00:00:00.000Z'),
sender: {
avatar: 'https://avatars.githubusercontent.com/u/10047061?v=4',
name: 'John Doe',
},
},
onPress: () => {},
},
} as any;
export { Normal };
export default meta;

View File

@@ -0,0 +1,71 @@
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Avatar, View } from '../../base';
import { Typography } from '../../typography';
import { formatRelativeTime } from '../../utils/time';
type MessageProps = {
message: {
text: React.ReactNode;
timestamp: Date;
sender: {
avatar?: string;
name: string;
};
};
onPress?: () => void;
};
const MessageContainer = styled(View)`
background-color: ${({ theme }) => theme.colors.bg.highlight100};
margin-bottom: 15px;
position: relative;
`;
const ArrowDown = styled.div`
position: absolute;
bottom: -10px;
left: 30px;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid ${({ theme }) => theme.colors.bg.highlight100};
`;
const Message: React.FC<MessageProps> = ({ message, onPress }) => {
const [time, setTime] = useState<string>(
formatRelativeTime(message.timestamp),
);
useEffect(() => {
const interval = setInterval(() => {
setTime(formatRelativeTime(message.timestamp));
}, 5000);
return () => clearInterval(interval);
});
return (
<View>
<MessageContainer $p="md" onClick={onPress}>
{message.text}
<Typography variant="tiny">{time}</Typography>
<ArrowDown />
</MessageContainer>
<View $fr $gap="sm" $items="center" $px="md">
<Avatar
size="sm"
url={message.sender.avatar}
name={message.sender.name}
/>
<View>
<Typography variant="overline">{message.sender.name}</Typography>
</View>
</View>
</View>
);
};
export { Message };

View File

@@ -0,0 +1,237 @@
{
"id": 30433642,
"name": "Build",
"node_id": "MDEyOldvcmtmbG93IFJ1bjI2OTI4OQ==",
"check_suite_id": 42,
"check_suite_node_id": "MDEwOkNoZWNrU3VpdGU0Mg==",
"head_branch": "main",
"head_sha": "acb5820ced9479c074f688cc328bf03f341a511d",
"path": ".github/workflows/build.yml@main",
"run_number": 562,
"event": "push",
"display_title": "Update README.md",
"status": "queued",
"conclusion": null,
"workflow_id": 159038,
"url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642",
"html_url": "https://github.com/octo-org/octo-repo/actions/runs/30433642",
"pull_requests": [],
"created_at": "2020-01-22T19:33:08Z",
"updated_at": "2020-01-22T19:33:08Z",
"actor": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"run_attempt": 1,
"referenced_workflows": [
{
"path": "octocat/Hello-World/.github/workflows/deploy.yml@main",
"sha": "86e8bc9ecf7d38b1ed2d2cfb8eb87ba9b35b01db",
"ref": "refs/heads/main"
},
{
"path": "octo-org/octo-repo/.github/workflows/report.yml@v2",
"sha": "79e9790903e1c3373b1a3e3a941d57405478a232",
"ref": "refs/tags/v2"
},
{
"path": "octo-org/octo-repo/.github/workflows/secure.yml@1595d4b6de6a9e9751fb270a41019ce507d4099e",
"sha": "1595d4b6de6a9e9751fb270a41019ce507d4099e"
}
],
"run_started_at": "2020-01-22T19:33:08Z",
"triggering_actor": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"jobs_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/jobs",
"logs_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/logs",
"check_suite_url": "https://api.github.com/repos/octo-org/octo-repo/check-suites/414944374",
"artifacts_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/artifacts",
"cancel_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/cancel",
"rerun_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/rerun",
"previous_attempt_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/attempts/1",
"workflow_url": "https://api.github.com/repos/octo-org/octo-repo/actions/workflows/159038",
"head_commit": {
"id": "acb5820ced9479c074f688cc328bf03f341a511d",
"tree_id": "d23f6eedb1e1b9610bbc754ddb5197bfe7271223",
"message": "Create linter.yaml",
"timestamp": "2020-01-22T19:33:05Z",
"author": {
"name": "Octo Cat",
"email": "octocat@github.com"
},
"committer": {
"name": "GitHub",
"email": "noreply@github.com"
}
},
"repository": {
"id": 1296269,
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5",
"name": "Hello-World",
"full_name": "octocat/Hello-World",
"owner": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/octocat/Hello-World",
"description": "This your first repo!",
"fork": false,
"url": "https://api.github.com/repos/octocat/Hello-World",
"archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}",
"assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}",
"blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}",
"collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}",
"commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}",
"compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}",
"contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors",
"deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments",
"downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads",
"events_url": "https://api.github.com/repos/octocat/Hello-World/events",
"forks_url": "https://api.github.com/repos/octocat/Hello-World/forks",
"git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}",
"git_url": "git:github.com/octocat/Hello-World.git",
"issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}",
"issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}",
"keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}",
"labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}",
"languages_url": "https://api.github.com/repos/octocat/Hello-World/languages",
"merges_url": "https://api.github.com/repos/octocat/Hello-World/merges",
"milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}",
"pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}",
"releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}",
"ssh_url": "git@github.com:octocat/Hello-World.git",
"stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers",
"statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription",
"tags_url": "https://api.github.com/repos/octocat/Hello-World/tags",
"teams_url": "https://api.github.com/repos/octocat/Hello-World/teams",
"trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}",
"hooks_url": "http://api.github.com/repos/octocat/Hello-World/hooks"
},
"head_repository": {
"id": 217723378,
"node_id": "MDEwOlJlcG9zaXRvcnkyMTc3MjMzNzg=",
"name": "octo-repo",
"full_name": "octo-org/octo-repo",
"private": true,
"owner": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/octo-org/octo-repo",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/octo-org/octo-repo",
"forks_url": "https://api.github.com/repos/octo-org/octo-repo/forks",
"keys_url": "https://api.github.com/repos/octo-org/octo-repo/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/octo-org/octo-repo/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/octo-org/octo-repo/teams",
"hooks_url": "https://api.github.com/repos/octo-org/octo-repo/hooks",
"issue_events_url": "https://api.github.com/repos/octo-org/octo-repo/issues/events{/number}",
"events_url": "https://api.github.com/repos/octo-org/octo-repo/events",
"assignees_url": "https://api.github.com/repos/octo-org/octo-repo/assignees{/user}",
"branches_url": "https://api.github.com/repos/octo-org/octo-repo/branches{/branch}",
"tags_url": "https://api.github.com/repos/octo-org/octo-repo/tags",
"blobs_url": "https://api.github.com/repos/octo-org/octo-repo/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/octo-org/octo-repo/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/octo-org/octo-repo/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/octo-org/octo-repo/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/octo-org/octo-repo/statuses/{sha}",
"languages_url": "https://api.github.com/repos/octo-org/octo-repo/languages",
"stargazers_url": "https://api.github.com/repos/octo-org/octo-repo/stargazers",
"contributors_url": "https://api.github.com/repos/octo-org/octo-repo/contributors",
"subscribers_url": "https://api.github.com/repos/octo-org/octo-repo/subscribers",
"subscription_url": "https://api.github.com/repos/octo-org/octo-repo/subscription",
"commits_url": "https://api.github.com/repos/octo-org/octo-repo/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/octo-org/octo-repo/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/octo-org/octo-repo/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/octo-org/octo-repo/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/octo-org/octo-repo/contents/{+path}",
"compare_url": "https://api.github.com/repos/octo-org/octo-repo/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/octo-org/octo-repo/merges",
"archive_url": "https://api.github.com/repos/octo-org/octo-repo/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/octo-org/octo-repo/downloads",
"issues_url": "https://api.github.com/repos/octo-org/octo-repo/issues{/number}",
"pulls_url": "https://api.github.com/repos/octo-org/octo-repo/pulls{/number}",
"milestones_url": "https://api.github.com/repos/octo-org/octo-repo/milestones{/number}",
"notifications_url": "https://api.github.com/repos/octo-org/octo-repo/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/octo-org/octo-repo/labels{/name}",
"releases_url": "https://api.github.com/repos/octo-org/octo-repo/releases{/id}",
"deployments_url": "https://api.github.com/repos/octo-org/octo-repo/deployments"
}
}

View File

@@ -0,0 +1,20 @@
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

@@ -0,0 +1,56 @@
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

@@ -0,0 +1,5 @@
export * from './notification';
export * from './profile';
export * from './pull-request';
export * from './login';
export * from './not-logged-in';

View File

@@ -0,0 +1,35 @@
import { GithubLogin as GithubLoginComponent } from '@refocus/sdk';
import { SiGithub } from 'react-icons/si';
import { useCallback, useState } from 'react';
import { Button, Dialog, View } from '../../base';
const GithubLogin: GithubLoginComponent = ({ setToken, cancel }) => {
const [value, setValue] = useState('');
const save = useCallback(() => {
setToken(value);
}, [setToken, value]);
return (
<Dialog open={true}>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<View $fc $gap="md">
<View
as="input"
$u
placeholder="Personal Access Token"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<Dialog.Buttons>
<Button icon={<SiGithub />} onClick={save} title="Save" />
</Dialog.Buttons>
</View>
<Dialog.CloseButton onClick={cancel} />
</Dialog.Content>
</Dialog.Portal>
</Dialog>
);
};
export { GithubLogin };

View File

@@ -0,0 +1,26 @@
import { useGithub, useWidget, useWidgetId } from '@refocus/sdk';
import { SiGithub } from 'react-icons/si';
import { Button, View } from '../../base';
import { Typography } from '../../typography';
import { styled } from 'styled-components';
const Description = styled(Typography)`
text-align: center;
`;
const NotLoggedIn: React.FC = () => {
const { login } = useGithub();
const type = useWidgetId();
const widget = useWidget(type);
return (
<View $p="md" $fc $items="center" $gap="md">
<Description>
You need to be logged in to Github to see {widget?.name}
</Description>
<Button icon={<SiGithub />} onClick={login} title="Login" />
</View>
);
};
export { NotLoggedIn };

View File

@@ -0,0 +1,22 @@
import { Card } from '../../base/card';
import { AsyncResponse } from '../../utils/types';
import { Octokit } from 'octokit';
type GithubNotification = {
notification: AsyncResponse<
Octokit['rest']['activity']['listNotificationsForAuthenticatedUser']
>['data'][0];
};
const GithubNotification: React.FC<GithubNotification> = ({ notification }) => {
return (
<Card>
<div>{notification.repository.full_name}</div>
<div>{notification.subject.title}</div>
<div>{notification.reason}</div>
<div>{notification.updated_at}</div>
</Card>
);
};
export { GithubNotification };

View File

@@ -0,0 +1,45 @@
import type { StoryFn, Meta } from '@storybook/react';
import { Profile } from './index';
import { GithubTypes } from '@refocus/sdk';
const meta = {
title: 'GitHub/Profile',
component: Profile,
} satisfies Meta<typeof Profile>;
type Story = StoryFn<typeof Profile>;
type ProfileData = Partial<GithubTypes.Profile>;
const profile: ProfileData = {
name: 'John Doe',
avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4',
login: 'johndoe',
};
const Normal: Story = {
args: {
profile,
},
} as any;
const WithoutName: Story = {
args: {
profile: {
...profile,
name: undefined,
} as GithubTypes.Profile,
},
} as any;
const WithoutImage: Story = {
args: {
profile: {
...profile,
avatar_url: undefined,
},
},
} as any;
export { Normal, WithoutName, WithoutImage };
export default meta;

View File

@@ -0,0 +1,33 @@
import { GithubTypes } from '@refocus/sdk';
import { useCallback } from 'react';
import { Avatar, Card, View } from '../../base';
import { Typography } from '../../typography';
import { LuGithub } from 'react-icons/lu';
type ProfileProps = {
profile: GithubTypes.Profile;
onPress?: (profile: GithubTypes.Profile) => void;
};
const Profile: React.FC<ProfileProps> = ({ profile, onPress }) => {
const onPressHandler = useCallback(() => {
onPress?.(profile);
}, [onPress, profile]);
return (
<Card $fr $items="center" $gap="md" $p="md" onClick={onPressHandler}>
<Avatar
decal={<LuGithub />}
url={profile.avatar_url}
name={profile.name || profile.login}
/>
<View $fr $fc>
<Typography variant="title">{profile.name || profile.login}</Typography>
{profile.name && profile.name !== profile.login && (
<Typography variant="subtitle">{profile.login}</Typography>
)}
</View>
</Card>
);
};
export { Profile };

View File

@@ -0,0 +1,536 @@
{
"url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347",
"id": 1,
"node_id": "MDExOlB1bGxSZXF1ZXN0MQ==",
"html_url": "https://github.com/octocat/Hello-World/pull/1347",
"diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff",
"patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch",
"issue_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347",
"commits_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/commits",
"review_comments_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/comments",
"review_comment_url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}",
"comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments",
"statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e",
"number": 1347,
"state": "open",
"locked": true,
"title": "Amazing new feature",
"user": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"body": "Please pull these awesome changes in!",
"labels": [
{
"id": 208045946,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
"url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
"name": "bug",
"description": "Something isn't working",
"color": "f29513",
"default": true
}
],
"milestone": {
"url": "https://api.github.com/repos/octocat/Hello-World/milestones/1",
"html_url": "https://github.com/octocat/Hello-World/milestones/v1.0",
"labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels",
"id": 1002604,
"node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==",
"number": 1,
"state": "open",
"title": "v1.0",
"description": "Tracking milestone for version 1.0",
"creator": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"open_issues": 4,
"closed_issues": 8,
"created_at": "2011-04-10T20:09:31Z",
"updated_at": "2014-03-03T18:58:10Z",
"closed_at": "2013-02-12T13:22:01Z",
"due_on": "2012-10-09T23:39:01Z"
},
"active_lock_reason": "too heated",
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:01:12Z",
"closed_at": "2011-01-26T19:01:12Z",
"merged_at": "2011-01-26T19:01:12Z",
"merge_commit_sha": "e5bd3914e2e596debea16f433f57875b5b90bcd6",
"assignee": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"assignees": [
{
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
{
"login": "hubot",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/hubot_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/hubot",
"html_url": "https://github.com/hubot",
"followers_url": "https://api.github.com/users/hubot/followers",
"following_url": "https://api.github.com/users/hubot/following{/other_user}",
"gists_url": "https://api.github.com/users/hubot/gists{/gist_id}",
"starred_url": "https://api.github.com/users/hubot/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/hubot/subscriptions",
"organizations_url": "https://api.github.com/users/hubot/orgs",
"repos_url": "https://api.github.com/users/hubot/repos",
"events_url": "https://api.github.com/users/hubot/events{/privacy}",
"received_events_url": "https://api.github.com/users/hubot/received_events",
"type": "User",
"site_admin": true
}
],
"requested_reviewers": [
{
"login": "other_user",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/other_user_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/other_user",
"html_url": "https://github.com/other_user",
"followers_url": "https://api.github.com/users/other_user/followers",
"following_url": "https://api.github.com/users/other_user/following{/other_user}",
"gists_url": "https://api.github.com/users/other_user/gists{/gist_id}",
"starred_url": "https://api.github.com/users/other_user/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/other_user/subscriptions",
"organizations_url": "https://api.github.com/users/other_user/orgs",
"repos_url": "https://api.github.com/users/other_user/repos",
"events_url": "https://api.github.com/users/other_user/events{/privacy}",
"received_events_url": "https://api.github.com/users/other_user/received_events",
"type": "User",
"site_admin": false
}
],
"requested_teams": [
{
"id": 1,
"node_id": "MDQ6VGVhbTE=",
"url": "https://api.github.com/teams/1",
"html_url": "https://github.com/orgs/github/teams/justice-league",
"name": "Justice League",
"slug": "justice-league",
"description": "A great team.",
"privacy": "closed",
"notification_setting": "notifications_enabled",
"permission": "admin",
"members_url": "https://api.github.com/teams/1/members{/member}",
"repositories_url": "https://api.github.com/teams/1/repos"
}
],
"head": {
"label": "octocat:new-topic",
"ref": "new-topic",
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
"user": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 1296269,
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5",
"name": "Hello-World",
"full_name": "octocat/Hello-World",
"owner": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/octocat/Hello-World",
"description": "This your first repo!",
"fork": false,
"url": "https://api.github.com/repos/octocat/Hello-World",
"archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}",
"assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}",
"blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}",
"collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}",
"commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}",
"compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}",
"contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors",
"deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments",
"downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads",
"events_url": "https://api.github.com/repos/octocat/Hello-World/events",
"forks_url": "https://api.github.com/repos/octocat/Hello-World/forks",
"git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}",
"git_url": "git:github.com/octocat/Hello-World.git",
"issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}",
"issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}",
"keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}",
"labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}",
"languages_url": "https://api.github.com/repos/octocat/Hello-World/languages",
"merges_url": "https://api.github.com/repos/octocat/Hello-World/merges",
"milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}",
"pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}",
"releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}",
"ssh_url": "git@github.com:octocat/Hello-World.git",
"stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers",
"statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription",
"tags_url": "https://api.github.com/repos/octocat/Hello-World/tags",
"teams_url": "https://api.github.com/repos/octocat/Hello-World/teams",
"trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}",
"clone_url": "https://github.com/octocat/Hello-World.git",
"mirror_url": "git:git.example.com/octocat/Hello-World",
"hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks",
"svn_url": "https://svn.github.com/octocat/Hello-World",
"homepage": "https://github.com",
"language": null,
"forks_count": 9,
"stargazers_count": 80,
"watchers_count": 80,
"size": 108,
"default_branch": "master",
"open_issues_count": 0,
"topics": [
"octocat",
"atom",
"electron",
"api"
],
"has_issues": true,
"has_projects": true,
"has_wiki": true,
"has_pages": false,
"has_downloads": true,
"has_discussions": false,
"archived": false,
"disabled": false,
"pushed_at": "2011-01-26T19:06:43Z",
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:14:43Z",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"allow_rebase_merge": true,
"temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O",
"allow_squash_merge": true,
"allow_merge_commit": true,
"allow_forking": true,
"forks": 123,
"open_issues": 123,
"license": {
"key": "mit",
"name": "MIT License",
"url": "https://api.github.com/licenses/mit",
"spdx_id": "MIT",
"node_id": "MDc6TGljZW5zZW1pdA=="
},
"watchers": 123
}
},
"base": {
"label": "octocat:master",
"ref": "master",
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
"user": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 1296269,
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5",
"name": "Hello-World",
"full_name": "octocat/Hello-World",
"owner": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/octocat/Hello-World",
"description": "This your first repo!",
"fork": false,
"url": "https://api.github.com/repos/octocat/Hello-World",
"archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}",
"assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}",
"blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}",
"collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}",
"commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}",
"compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}",
"contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors",
"deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments",
"downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads",
"events_url": "https://api.github.com/repos/octocat/Hello-World/events",
"forks_url": "https://api.github.com/repos/octocat/Hello-World/forks",
"git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}",
"git_url": "git:github.com/octocat/Hello-World.git",
"issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}",
"issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}",
"keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}",
"labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}",
"languages_url": "https://api.github.com/repos/octocat/Hello-World/languages",
"merges_url": "https://api.github.com/repos/octocat/Hello-World/merges",
"milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}",
"pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}",
"releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}",
"ssh_url": "git@github.com:octocat/Hello-World.git",
"stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers",
"statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription",
"tags_url": "https://api.github.com/repos/octocat/Hello-World/tags",
"teams_url": "https://api.github.com/repos/octocat/Hello-World/teams",
"trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}",
"clone_url": "https://github.com/octocat/Hello-World.git",
"mirror_url": "git:git.example.com/octocat/Hello-World",
"hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks",
"svn_url": "https://svn.github.com/octocat/Hello-World",
"homepage": "https://github.com",
"language": null,
"forks_count": 9,
"stargazers_count": 80,
"watchers_count": 80,
"size": 108,
"default_branch": "master",
"open_issues_count": 0,
"topics": [
"octocat",
"atom",
"electron",
"api"
],
"has_issues": true,
"has_projects": true,
"has_wiki": true,
"has_pages": false,
"has_downloads": true,
"has_discussions": false,
"archived": false,
"disabled": false,
"pushed_at": "2011-01-26T19:06:43Z",
"created_at": "2011-01-26T19:01:12Z",
"updated_at": "2011-01-26T19:14:43Z",
"permissions": {
"admin": false,
"push": false,
"pull": true
},
"allow_rebase_merge": true,
"temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O",
"allow_squash_merge": true,
"allow_merge_commit": true,
"forks": 123,
"open_issues": 123,
"license": {
"key": "mit",
"name": "MIT License",
"url": "https://api.github.com/licenses/mit",
"spdx_id": "MIT",
"node_id": "MDc6TGljZW5zZW1pdA=="
},
"watchers": 123
}
},
"_links": {
"self": {
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347"
},
"html": {
"href": "https://github.com/octocat/Hello-World/pull/1347"
},
"issue": {
"href": "https://api.github.com/repos/octocat/Hello-World/issues/1347"
},
"comments": {
"href": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments"
},
"review_comments": {
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/comments"
},
"review_comment": {
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}"
},
"commits": {
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/commits"
},
"statuses": {
"href": "https://api.github.com/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e"
}
},
"author_association": "OWNER",
"auto_merge": null,
"draft": false,
"merged": false,
"mergeable": true,
"rebaseable": true,
"mergeable_state": "clean",
"merged_by": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"comments": 10,
"review_comments": 0,
"maintainer_can_modify": true,
"commits": 3,
"additions": 100,
"deletions": 3,
"changed_files": 5
}

View File

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

View File

@@ -0,0 +1,32 @@
import { useCallback } from 'react';
import { GithubTypes } from '@refocus/sdk';
import { Avatar, Card, View } from '../../base';
import { Typography } from '../../typography';
type PullRequestProps = {
pullRequest: GithubTypes.PullRequest;
onPress?: (oullRequest: GithubTypes.PullRequest) => void;
};
const PullRequest: React.FC<PullRequestProps> = ({ pullRequest, onPress }) => {
const onPressHandler = useCallback(() => {
onPress?.(pullRequest);
}, [pullRequest, onPress]);
return (
<Card $fr $items="center" $p="md" $gap="md" onClick={onPressHandler}>
<Avatar
url={pullRequest.user.avatar_url}
decal={pullRequest.state === 'open' ? 'open' : 'closed'}
/>
<View $fc>
<Typography variant="overline">
{pullRequest.head.repo?.full_name}
{' - '}#{pullRequest.number}
</Typography>
<Typography variant="title">{pullRequest.title}</Typography>
</View>
</Card>
);
};
export { PullRequest };

9
packages/ui/src/index.ts Normal file
View File

@@ -0,0 +1,9 @@
export * from './base';
export * as Github from './github';
export * as Linear from './linear';
export * as Interface from './interface';
export * as Chat from './chat';
export * as Slack from './slack';
export { FocusProvider } from './provider';
export { UIProvider } from './theme/provider';
export { Typography, styles } from './typography';

View File

@@ -0,0 +1,84 @@
import { useState, useCallback, useEffect } from 'react';
import { Card, Dialog, View } from '../../base';
import { useGetWidgetsFromUrl } from '@refocus/sdk';
import { Typography } from '../../typography';
type AddWidgetFromUrlProps = {
onCreate: (name: string, data: any) => void;
children?: React.ReactNode;
};
const Root: React.FC<AddWidgetFromUrlProps> = ({ onCreate, children }) => {
const [url, setUrl] = useState('');
const [widgets, update] = useGetWidgetsFromUrl();
const handleSave = useCallback(
(id: string, data: any) => {
onCreate(id, data);
},
[onCreate],
);
useEffect(() => {
const parsed = new URL(url, 'http://example.com');
if (parsed.host === 'example.com') {
return;
}
update(parsed);
}, [url, update]);
return (
<Dialog>
{children}
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.CloseButton />
<View $fc $gap="sm">
<View $fr>
<View
as="input"
$f={1}
$u
placeholder="URL"
onChange={(e) => setUrl(e.target.value)}
value={url}
/>
</View>
{widgets.map((widget) => (
<View key={widget.id}>
<Dialog.Close>
<Card
$fr
$items="center"
$gap="md"
$p="md"
onClick={() => handleSave(widget.id, widget.data)}
>
<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>
</Dialog.Close>
</View>
))}
</View>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
);
};
const AddWidgetFromUrl = Object.assign(Root, {
Trigger: Dialog.Trigger,
});
export { AddWidgetFromUrl };

View File

@@ -0,0 +1,66 @@
import {
useAddBoard,
useBoards,
useRemoveBoard,
useSelectBoard,
useSelectedBoard,
} 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';
const NotificationBar = styled(View)``;
const App: React.FC = () => {
const boards = useBoards();
const selected = useSelectedBoard();
const selectBoard = useSelectBoard();
const addBoardAction = useAddBoard();
const removeBoard = useRemoveBoard();
const addBoard = useCallback(() => {
const name = prompt('Board name?');
if (!name) {
return;
}
addBoardAction(name);
}, [addBoardAction]);
return (
<View>
<View $f={1}>
<Tabs value={selected} onValueChange={selectBoard}>
<Tabs.List>
{Object.entries(boards).map(([id, board]) => (
<Tabs.Trigger key={id} value={id}>
{board.name}
<Tabs.Close onClick={() => removeBoard(id)} />
</Tabs.Trigger>
))}
<View
onClick={addBoard}
$fr
$justify="center"
$items="center"
$p="md"
$bg="highlight100"
>
<IoAddCircleOutline size={16} />
</View>
</Tabs.List>
{Object.entries(boards).map(([id, board]) => (
<Tabs.Content key={id} value={id}>
<Board id={id} board={board} />
</Tabs.Content>
))}
</Tabs>
</View>
<NotificationBar></NotificationBar>
</View>
);
};
export { App };

View File

@@ -0,0 +1,62 @@
import {
Board,
useAddWidget,
useRemoveWidget,
useUpdateWidget,
} from '@refocus/sdk';
import { IoAddCircleOutline } from 'react-icons/io5';
import { View } from '../../base';
import { Widget } from '../widget';
import { AddWidgetFromUrl } from '../add-from-url';
import { styled } from 'styled-components';
type BoardProps = {
board: Board;
id: string;
};
const Wrapper = styled(View)`
flex-wrap: wrap;
`;
const ItemWrapper = styled(View)`
max-width: 400px;
overflow-y: auto;
max-height: 500px;
`;
const Board: React.FC<BoardProps> = ({ board, id }) => {
const setWidgetData = useUpdateWidget();
const removeWidget = useRemoveWidget();
const addWidget = useAddWidget();
return (
<View>
<View $p="md">
<AddWidgetFromUrl onCreate={(type, data) => addWidget(id, type, data)}>
<AddWidgetFromUrl.Trigger>
<View $fr $items="center" $p="sm" $gap="sm">
<IoAddCircleOutline />
Add from URL
</View>
</AddWidgetFromUrl.Trigger>
</AddWidgetFromUrl>
</View>
<Wrapper $fr>
{Object.entries(board.widgets).map(([widgetId, widget]) => (
<ItemWrapper key={widgetId}>
<Widget
key={widgetId}
id={widget.type}
data={widget.data}
setData={(data) => setWidgetData(id, widgetId, data)}
onRemove={() => removeWidget(id, widgetId)}
/>
</ItemWrapper>
))}
</Wrapper>
</View>
);
};
export { Board };

View File

@@ -0,0 +1,81 @@
import { useState, useCallback } from 'react';
import { Dialog } from '../../base';
import {
WidgetEditor,
WidgetProvider,
useWidgets,
} from '@refocus/sdk';
type CreateWidgetProps = {
onCreate: (name: string, data: any) => void;
children?: React.ReactNode;
};
type WidgetEditorProps = {
id: string;
onSave: (data: any) => void;
};
const Editor: React.FC<WidgetEditorProps> = ({ id, onSave }) => {
return (
<WidgetProvider id={id}>
<WidgetEditor onSave={onSave} />
</WidgetProvider>
);
};
type WidgetSelectorProps = {
onSelect: (id: string) => void;
};
const WidgetSelector: React.FC<WidgetSelectorProps> = ({ onSelect }) => {
const widgets = useWidgets();
const handleSelect = useCallback(
(id: string) => {
onSelect(id);
},
[onSelect],
);
return (
<div>
{widgets.map((widget) => (
<button key={widget.id} onClick={() => handleSelect(widget.id)}>
{widget.name}
</button>
))}
</div>
);
};
const Root: React.FC<CreateWidgetProps> = ({ onCreate, children }) => {
const [id, setId] = useState<string>('');
const handleSave = useCallback(
(data: any) => {
onCreate(id, data);
},
[id, onCreate],
);
return (
<Dialog>
{children}
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.CloseButton />
{id && <Editor id={id} onSave={handleSave} />}
{!id && <WidgetSelector onSelect={setId} />}
</Dialog.Content>
</Dialog.Portal>
</Dialog>
);
};
const CreateWidget = Object.assign(Root, {
Trigger: Dialog.Trigger,
});
export { CreateWidget };

View File

@@ -0,0 +1,6 @@
export * from './create-widget';
export * from './add-from-url';
export * from './notifications';
export * from './widget';
export * from './board';
export * from './app';

View File

@@ -0,0 +1,26 @@
import { useNotificationDismiss, useNotifications } from '@refocus/sdk';
import { Card, List } from '../../base';
import { Typography } from '../../typography';
const Notifications: React.FC = () => {
const notifications = useNotifications();
const dismiss = useNotificationDismiss();
return (
<List>
{notifications.map((notification, index) => (
<Card
key={notification.id || index}
onClick={() => dismiss(notification.id || '')}
>
{notification.title && (
<Typography variant="title">{notification.title}</Typography>
)}
{notification.message}
</Card>
))}
</List>
);
};
export { Notifications };

View File

@@ -0,0 +1,96 @@
import styled, { useTheme } from 'styled-components';
import {
WidgetEditor,
WidgetProvider,
WidgetView,
useWidget,
} from '@refocus/sdk';
import { VscTrash } from 'react-icons/vsc';
import { CgMoreO } from 'react-icons/cg';
import { Dialog, View } from '../../base';
import { DropdownMenu } from '../../base';
import { useCallback, useMemo, useState } from 'react';
type WidgetProps = {
id: string;
data: any;
setData?: (data: any) => void;
className?: string;
onRemove?: () => void;
};
const Wrapper = styled(View)`
background: ${({ theme }) => theme.colors.bg.base};
`;
const Widget: React.FC<WidgetProps> = ({
id,
data,
setData,
className,
onRemove,
}) => {
const theme = useTheme();
const [showEdit, setShowEdit] = useState(false);
const widget = useWidget(id);
const hasMenu = useMemo(
() => !!(widget?.edit && setData) || onRemove,
[widget, onRemove, setData],
);
const onSave = useCallback(
(nextData: any) => {
setData?.(nextData);
setShowEdit(false);
},
[setData],
);
return (
<WidgetProvider id={id} data={data} setData={setData}>
<Wrapper className={className} $fr>
<View $f={1}>
<WidgetView />
</View>
<View $fc>
{hasMenu && (
<DropdownMenu>
<DropdownMenu.Trigger>
<View $p="sm">
<CgMoreO />
</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.Portal>
</DropdownMenu>
)}
</View>
</Wrapper>
<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>
</WidgetProvider>
);
};
export { Widget };

View File

@@ -0,0 +1,4 @@
export * from './issue';
export * from './notification';
export * from './login';
export * from './not-logged-in';

View File

@@ -0,0 +1,19 @@
import type { Issue as IssueType } from '@linear/sdk';
import { Link } from 'react-router-dom';
import { IssueSearchResult } from '@linear/sdk/dist/_generated_documents';
type IssueProps = {
issue: IssueType | IssueSearchResult;
};
const Issue: React.FC<IssueProps> = ({ issue }) => {
return (
<div>
<Link to={`/linear/issue?id=${issue.id}`}>
<h3 className="text-lg font-bold">{issue.title}</h3>
</Link>
{issue.description}
</div>
);
};
export { Issue };

View File

@@ -0,0 +1,35 @@
import { LinearLogin as LinearLoginComponent } from '@refocus/sdk';
import { useCallback, useState } from 'react';
import { Button, Dialog, View } from '../../base';
import { SiLinear } from 'react-icons/si';
const LinearLogin: LinearLoginComponent = ({ setApiKey, cancel }) => {
const [value, setValue] = useState('');
const save = useCallback(() => {
setApiKey(value);
}, [setApiKey, value]);
return (
<Dialog open={true}>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<View $fc $gap="md">
<View
as="input"
$u
value={value}
placeholder="API Token"
onChange={(e) => setValue(e.target.value)}
/>
<Dialog.Buttons>
<Button icon={<SiLinear />} onClick={save} title="Save" />
</Dialog.Buttons>
</View>
<Dialog.CloseButton onClick={cancel} />
</Dialog.Content>
</Dialog.Portal>
</Dialog>
);
};
export { LinearLogin };

View File

@@ -0,0 +1,26 @@
import { useLinear, useWidget, useWidgetId } from '@refocus/sdk';
import { SiLinear } from 'react-icons/si';
import { Button, View } from '../../base';
import { Typography } from '../../typography';
import { styled } from 'styled-components';
const Description = styled(Typography)`
text-align: center;
`;
const NotLoggedIn: React.FC = () => {
const { login } = useLinear();
const type = useWidgetId();
const widget = useWidget(type);
return (
<View $p="md" $fc $items="center" $gap="md">
<Description>
You need to be logged in to Linear to see {widget?.name}
</Description>
<Button icon={<SiLinear />} onClick={login} title="Login" />
</View>
);
};
export { NotLoggedIn };

View File

@@ -0,0 +1,16 @@
import { Card } from '../../base/card';
import { AsyncResponse, Expand } from '../../utils/types';
import { LinearClient } from '@linear/sdk';
type LinearNotificationProps = {
notification: Expand<
AsyncResponse<LinearClient['notifications']>['nodes'][0]
>;
};
const LinearNotification: React.FC<LinearNotificationProps> = ({
notification,
}) => {
return <Card>Hello</Card>;
};
export { LinearNotification };

View File

@@ -0,0 +1,52 @@
import { DashboardProvider, Widget } from '@refocus/sdk';
import { UIProvider } from './theme/provider';
import { useCallback, useMemo } from 'react';
import { GithubLogin } from './github';
import { LinearLogin } from './linear';
import { SlackLogin } from './slack';
type FocusProviderProps = {
children: React.ReactNode;
widgets: Widget<any>[];
};
const FocusProvider: React.FC<FocusProviderProps> = ({ children, widgets }) => {
const save = useCallback((data: any) => {
localStorage.setItem('boards', JSON.stringify(data));
}, []);
const logins = useMemo(
() => ({
github: GithubLogin,
linear: LinearLogin,
slack: SlackLogin,
}),
[],
);
const load = useCallback(() => {
const data = localStorage.getItem('boards');
if (data) {
return JSON.parse(data);
}
return {
boards: {},
selected: undefined,
};
}, []);
return (
<UIProvider>
<DashboardProvider
load={load}
save={save}
widgets={widgets}
logins={logins}
>
{children}
</DashboardProvider>
</UIProvider>
);
};
export { FocusProvider };

View File

@@ -0,0 +1,2 @@
export * from './login';
export * from './not-logged-in';

View File

@@ -0,0 +1,34 @@
import { SlackLogin as SlackLoginComponent } from '@refocus/sdk';
import { useCallback, useState } from 'react';
import { Button, Dialog, View } from '../../base';
const SlackLogin: SlackLoginComponent = ({ setToken, cancel }) => {
const [value, setValue] = useState('');
const save = useCallback(() => {
setToken(value);
}, [setToken, value]);
return (
<Dialog open={true}>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<View $fc $gap="md">
<View
as="input"
$u
placeholder="Token"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<Dialog.Buttons>
<Button onClick={save} title="Save" />
</Dialog.Buttons>
</View>
<Dialog.CloseButton onClick={cancel} />
</Dialog.Content>
</Dialog.Portal>
</Dialog>
);
};
export { SlackLogin };

View File

@@ -0,0 +1,26 @@
import { useSlack, useWidget, useWidgetId } from '@refocus/sdk';
import { SiSlack } from 'react-icons/si';
import { Button, View } from '../../base';
import { Typography } from '../../typography';
import { styled } from 'styled-components';
const Description = styled(Typography)`
text-align: center;
`;
const NotLoggedIn: React.FC = () => {
const { login } = useSlack();
const type = useWidgetId();
const widget = useWidget(type);
return (
<View $p="md" $fc $items="center" $gap="md">
<Description>
You need to be logged in to Slack to see {widget?.name}
</Description>
<Button icon={<SiSlack />} onClick={login} title="Login" />
</View>
);
};
export { NotLoggedIn };

View File

@@ -0,0 +1,52 @@
const dark = {
colors: {
simple: {
red: 'red',
green: 'green',
},
bg: {
base: 'rgb(25, 26, 35)',
base100: 'rgb(38, 39, 54)',
hover: 'rgb(38, 39, 54)',
highlight: 'rgb(158, 140, 252)',
highlight100: 'rgb(158, 140, 252, 0.1)',
},
text: {
base: 'rgb(210, 211, 224)',
highlight: '#fff',
disabled: 'rgb(210, 211, 224, 0.5)',
},
},
fonts: {
body: 'Roboto, sans-serif',
},
fontSizes: {
base: 1,
},
fontWeights: {
base: 400,
bold: 700,
},
lineHeights: {
base: 1.5,
},
radii: {
base: 4,
md: 8,
lg: 16,
},
shadows: {
base: '0 0 0 1px rgba(0, 0, 0, 0.1)',
},
units: {
radii: 'px',
space: 'rem',
fontSize: 'rem',
},
space: {
sm: 0.5,
md: 1,
},
};
export { dark };

View File

@@ -0,0 +1,2 @@
export { dark } from './dark';
export type { Theme } from './theme';

View File

@@ -0,0 +1,43 @@
import { ThemeProvider, createGlobalStyle } from 'styled-components';
import { dark } from '../theme';
import { styles } from '../typography';
type UIProviderProps = {
children: React.ReactNode;
};
// @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;
}
button {
all: unset;
cursor: pointer;
}
html, body {
height: 100%;
overscroll-behavior: none;
}
body {
background-color: ${({ theme }) => theme.colors.bg.base};
color: ${({ theme }) => theme.colors.text.base};
margin: 0;
padding: 0;
${styles.body}
}
`;
const UIProvider: React.FC<UIProviderProps> = ({ children }) => {
return (
<ThemeProvider theme={dark}>
<GlobalStyle />
{children}
</ThemeProvider>
);
};
export { UIProvider };

5
packages/ui/src/theme/styled.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import {} from 'styled-components';
import type { Theme } from './theme';
declare module 'styled-components' {
export interface DefaultTheme extends Theme {} // extends the global DefaultTheme with our ThemeType.
}

View File

@@ -0,0 +1,5 @@
import type { dark } from './dark';
type Theme = typeof dark;
export type { Theme };

View File

@@ -0,0 +1,54 @@
import { styled, css } from 'styled-components';
import { View } from '../base/view';
const styles = {
header: css`
font-size: 32px;
font-weight: bold;
`,
body: css`
font-size: 14px;
font-family: ${({ theme }) => theme.fonts.body};
font-size: ${({ theme }) => theme.fontSizes.base};
line-height: ${({ theme }) => theme.lineHeights.base};
font-size: 16px;
overflow-wrap: break-word;
`,
title: css`
font-size: 15px;
font-weight: bold;
`,
subtitle: css`
font-weight: normal;
font-size: 15px;
`,
dialogTitle: css`
font-size: 24px;
font-weight: 500;
`,
tiny: css`
font-size: 10px;
`,
overline: css`
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
`,
} satisfies Record<string, ReturnType<typeof css>>;
type TypographyProps = {
variant?: keyof typeof styles;
};
const getStyle = (variant: TypographyProps['variant']) => {
if (variant && styles[variant]) {
return styles[variant];
}
return styles.body;
};
const Typography = styled(View)<TypographyProps>`
${({ variant }) => getStyle(variant)}
`;
export { Typography, styles };

View File

@@ -0,0 +1,33 @@
const formatRelativeTime = (date: Date) => {
const rtf = new Intl.RelativeTimeFormat('en', { style: 'long' });
const diff = Date.now() - date.getTime();
const diffInMinutes = Math.floor(diff / 1000 / 60);
const diffInHours = Math.floor(diffInMinutes / 60);
const diffInDays = Math.floor(diffInHours / 24);
const diffInMonths = Math.floor(diffInDays / 30);
const diffInYears = Math.floor(diffInMonths / 12);
if (diffInMinutes < 1) {
return 'just now';
}
if (diffInMinutes < 60) {
return rtf.format(-diffInMinutes, 'minute');
}
if (diffInHours < 24) {
return rtf.format(-diffInHours, 'hour');
}
if (diffInDays < 30) {
return rtf.format(-diffInDays, 'day');
}
if (diffInMonths < 12) {
return rtf.format(-diffInMonths, 'month');
}
return rtf.format(-diffInYears, 'year');
};
export { formatRelativeTime };

View File

@@ -0,0 +1,16 @@
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// expands object types recursively
type ExpandRecursively<T> = T extends object
? T extends infer O
? { [K in keyof O]: ExpandRecursively<O[K]> }
: never
: T;
type AsyncResponse<T> = T extends () => Promise<infer U> ? U : never;
type FirstParameter<T> = T extends (arg1: infer U, ...args: any[]) => any
? U
: never;
export type { Expand, ExpandRecursively, AsyncResponse, FirstParameter };

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"declarationDir": "./dist/esm/types",
"outDir": "dist/esm",
"resolveJsonModule": true,
},
"extends": "@refocus/config/esm",
"include": [
"src/**/*"
],
"exclude": [
"!src/**/*.stories.tsx"
]
}

15
packages/ui/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"declarationDir": "./dist/cjs/types",
"outDir": "dist/cjs",
"jsx": "react-jsx",
"resolveJsonModule": true,
},
"extends": "@refocus/config/cjs",
"include": [
"src/**/*"
],
"exclude": [
"!src/**/*.stories.tsx"
]
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
const ASSET_URL = process.env.ASSET_URL || '';
export default defineConfig({
base: ASSET_URL ? `${ASSET_URL}/` : './',
plugins: [react()],
});