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

View File

@@ -0,0 +1,14 @@
module.exports = {
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn',
},
}

24
packages/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

20
packages/app/index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Loading</title>
</head>
<body>
<div id="root"></div>
<script>
window.process = {
env: {
NODE_ENV: 'production',
},
};
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

45
packages/app/package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"bundle": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@refocus/sdk": "workspace:^",
"@refocus/ui": "workspace:^",
"@refocus/widgets": "workspace:^",
"framer": "^2.3.0",
"framer-motion": "^10.12.16",
"localforage": "^1.10.0",
"match-sorter": "^6.3.1",
"popmotion": "^11.0.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-masonry-component": "^6.3.0",
"react-responsive-masonry": "^2.1.7",
"react-router-dom": "^6.13.0",
"sort-by": "^1.2.0"
},
"devDependencies": {
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@types/react-responsive-masonry": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react-swc": "^3.0.0",
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"isomorphic-fetch": "^3.0.0",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
"stream-browserify": "^3.0.0",
"typescript": "^5.0.2",
"vite": "^4.3.9"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

12
packages/app/src/app.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { widgets } from '@refocus/widgets';
import { FocusProvider, Interface } from '@refocus/ui';
const App: React.FC = () => {
return (
<FocusProvider widgets={widgets as any}>
<Interface.App />
</FocusProvider>
);
};
export default App;

View File

@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './app';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

1
packages/app/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,28 @@
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()],
resolve: {
alias: {
fetch: 'isomorphic-fetch',
stream: 'stream-browserify',
path: 'path-browserify',
os: 'os-browserify',
},
},
define: {
global: 'globalThis',
},
optimizeDeps: {
esbuildOptions: {
// Node.js global to browser globalThis
define: {
global: 'globalThis',
},
},
},
});

View File

@@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"lib": ["ES2019", "DOM"],
"target": "ES2019",
"module": "CommonJS",
"moduleResolution": "Node"
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"lib": ["ES2022", "DOM"],
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "NodeNext"
}
}

View File

@@ -0,0 +1,4 @@
{
"name": "@refocus/config",
"version": "0.0.1"
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"checkJs": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
"skipLibCheck": true,
"sourceMap": true,
"strict": true
},
"include": [],
"ts-node": {
"files": true
}
}

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

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

38
packages/sdk/package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"devDependencies": {
"@refocus/config": "workspace:^",
"@types/react": "^18.0.37",
"@types/uuid": "^9.0.2",
"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/sdk",
"scripts": {
"build": "pnpm build:esm && pnpm build:cjs",
"build:cjs": "tsc -p tsconfig.json",
"build:esm": "tsc -p tsconfig.esm.json"
},
"types": "./dist/cjs/types/index.d.ts",
"dependencies": {
"@linear/sdk": "^5.0.0",
"@sinclair/typebox": "^0.28.15",
"@slack/web-api": "^6.8.1",
"octokit": "^2.0.19",
"uuid": "^9.0.0"
}
}

View File

@@ -0,0 +1,140 @@
import {
createContext,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { v4 as uuid } from 'uuid';
import { Boards, BoardsLoad, BoardsSave } from './types';
type BoardsContextValue = {
selected?: string;
boards: Boards;
addBoard: (name: string) => void;
selectBoard: (id: string) => void;
removeBoard: (id: string) => void;
addWidget: (boardId: string, type: string, data: string) => void;
removeWidget: (boardId: string, widgetId: string) => void;
updateWidget: (boardId: string, widgetId: string, data: string) => void;
};
type BoardsProviderProps = {
children: React.ReactNode;
load: BoardsLoad;
save: BoardsSave;
};
const BoardsContext = createContext<BoardsContextValue | null>(null);
const BoardsProvider: React.FC<BoardsProviderProps> = ({
children,
load,
save,
}) => {
const [boards, setBoards] = useState<Boards>(load().boards);
const [selected, setSelected] = useState<string | undefined>(load().selected);
const addBoard = useCallback((name: string) => {
const id = uuid();
setBoards((currentBoards) => ({
...currentBoards,
[id]: {
name,
widgets: {},
},
}));
}, []);
const removeBoard = useCallback((id: string) => {
setBoards((currentBoards) => {
const copy = { ...currentBoards };
delete copy[id];
return copy;
});
}, []);
const addWidget = useCallback(
(boardId: string, type: string, data: string) => {
const id = uuid();
setBoards((currentBoards) => ({
...currentBoards,
[boardId]: {
...currentBoards[boardId],
widgets: {
...currentBoards[boardId].widgets,
[id]: {
type,
data,
},
},
},
}));
},
[],
);
const removeWidget = useCallback((boardId: string, widgetId: string) => {
setBoards((currentBoards) => {
const copy = { ...currentBoards };
delete copy[boardId].widgets[widgetId];
return copy;
});
}, []);
const updateWidget = useCallback(
(boardId: string, widgetId: string, data: string) => {
setBoards((currentBoards) => ({
...currentBoards,
[boardId]: {
...currentBoards[boardId],
widgets: {
...currentBoards[boardId].widgets,
[widgetId]: {
...currentBoards[boardId].widgets[widgetId],
data,
},
},
},
}));
},
[],
);
const selectBoard = useCallback((id: string) => {
setSelected(id);
}, []);
useEffect(() => {
save({ boards, selected });
}, [boards, selected, save]);
const value = useMemo(
() => ({
boards,
selected,
addBoard,
removeBoard,
addWidget,
removeWidget,
selectBoard,
updateWidget,
}),
[
boards,
selected,
addBoard,
removeBoard,
addWidget,
removeWidget,
selectBoard,
updateWidget,
],
);
return (
<BoardsContext.Provider value={value}>{children}</BoardsContext.Provider>
);
};
export { BoardsContext, BoardsProvider };

View File

@@ -0,0 +1,77 @@
import { useContext } from 'react';
import { BoardsContext } from './context';
const useBoards = () => {
const context = useContext(BoardsContext);
if (!context) {
throw new Error('useBoards must be used within a BoardsProvider');
}
return context.boards;
};
const useSelectedBoard = () => {
const context = useContext(BoardsContext);
if (!context) {
throw new Error('useCurrentBoard must be used within a BoardsProvider');
}
return context.selected;
};
const useAddWidget = () => {
const context = useContext(BoardsContext);
if (!context) {
throw new Error('useAddWidget must be used within a BoardsProvider');
}
return context.addWidget;
};
const useRemoveWidget = () => {
const context = useContext(BoardsContext);
if (!context) {
throw new Error('useRemoveWidget must be used within a BoardsProvider');
}
return context.removeWidget;
};
const useAddBoard = () => {
const context = useContext(BoardsContext);
if (!context) {
throw new Error('useAddBoard must be used within a BoardsProvider');
}
return context.addBoard;
};
const useRemoveBoard = () => {
const context = useContext(BoardsContext);
if (!context) {
throw new Error('useRemoveBoard must be used within a BoardsProvider');
}
return context.removeBoard;
};
const useSelectBoard = () => {
const context = useContext(BoardsContext);
if (!context) {
throw new Error('useSelectBoard must be used within a BoardsProvider');
}
return context.selectBoard;
};
const useUpdateWidget = () => {
const context = useContext(BoardsContext);
if (!context) {
throw new Error('useUpdateWidget must be used within a BoardsProvider');
}
return context.updateWidget;
};
export {
useBoards,
useSelectedBoard,
useAddWidget,
useRemoveWidget,
useAddBoard,
useRemoveBoard,
useSelectBoard,
useUpdateWidget,
};

View File

@@ -0,0 +1,12 @@
export { BoardsProvider } from './context';
export {
useBoards,
useSelectedBoard,
useAddWidget,
useRemoveWidget,
useAddBoard,
useRemoveBoard,
useSelectBoard,
useUpdateWidget,
} from './hooks';
export * from './types';

View File

@@ -0,0 +1,23 @@
type BoardData = { boards: Boards; selected?: string };
type BoardsLoad = () => BoardData;
type BoardsSave = (data: BoardData) => void;
type BoardWidget = {
type: string;
data: any;
};
type Board = {
name: string;
widgets: {
[key: string]: BoardWidget;
};
};
type Boards = {
[key: string]: Board;
};
export type { Board, Boards, BoardWidget, BoardsLoad, BoardsSave };

View File

@@ -0,0 +1,71 @@
import { createContext, useCallback, useMemo, useState } from 'react';
import { Octokit } from 'octokit';
type GithubLogin = React.ComponentType<{
setToken: (token: string) => void;
cancel: () => void;
}>;
type GithubClientContextValue = {
octokit?: Octokit;
login?: () => void;
logout: () => void;
};
type GithubClientProviderProps = {
children: React.ReactNode;
login: GithubLogin;
};
const GithubClientContext = createContext<GithubClientContextValue | null>(
null,
);
const GithubClientProvider: React.FC<GithubClientProviderProps> = ({
children,
login: GithubLoginComponent,
}) => {
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [pat, setPat] = useState(localStorage.getItem('github_pat') || '');
const octokit = useMemo(() => {
if (!pat) {
return;
}
localStorage.setItem('github_pat', pat);
return new Octokit({ auth: pat });
}, [pat]);
const logout = useCallback(() => {
setPat('');
localStorage.removeItem('github_pat');
}, [setPat]);
const login = useCallback(() => {
setIsLoggingIn(true);
}, [setIsLoggingIn]);
const onLogin = useCallback(
(nextPat: string) => {
setPat(nextPat);
setIsLoggingIn(false);
},
[setPat, setIsLoggingIn],
);
const cancelLogin = useCallback(() => {
setIsLoggingIn(false);
}, [setIsLoggingIn]);
return (
<GithubClientContext.Provider value={{ octokit, logout, login }}>
{isLoggingIn && (
<GithubLoginComponent cancel={cancelLogin} setToken={onLogin} />
)}
{children}
</GithubClientContext.Provider>
);
};
export type { GithubLogin };
export { GithubClientContext, GithubClientProvider };

View File

@@ -0,0 +1,64 @@
import { useCallback, useContext, useMemo, useState } from 'react';
import { GithubClientContext } from './context';
import { Octokit } from 'octokit';
const useGithub = () => {
const context = useContext(GithubClientContext);
if (!context) {
throw new Error(
'useGithubClient must be used within a GithubClientProvider',
);
}
const value = useMemo(
() => ({
client: context.octokit,
logout: context.logout,
login: context.login,
}),
[context.octokit, context.logout, context.login],
);
return value;
};
const useGithubQuery = <P, T = unknown>(
query: (client: Octokit, params: P) => Promise<T>,
) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<unknown | null>(null);
const [data, setData] = useState<T>();
const { client } = useGithub();
const fetch = useCallback(
async (params: P) => {
if (!client) {
throw new Error('Github client is not initialized');
}
try {
setLoading(true);
const data = await query(client, params);
setData(data);
return data;
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
},
[client, query],
);
const value = useMemo(
() => ({
loading,
error,
data,
fetch,
}),
[loading, error, data, fetch],
);
return value;
};
export { useGithub, useGithubQuery };

View File

@@ -0,0 +1,4 @@
export { GithubClientProvider, GithubLogin } from './context';
export { useGithub, useGithubQuery } from './hooks';
export { withGithub } from './with-github';
export * as GithubTypes from './types';

View File

@@ -0,0 +1,12 @@
import { Octokit } from 'octokit';
import { AsyncResponse } from '../../utils/types';
type PullRequest = AsyncResponse<Octokit['rest']['pulls']['get']>['data'];
type Commit = AsyncResponse<Octokit['rest']['repos']['getCommit']>['data'];
type WorkflowRun = AsyncResponse<
Octokit['rest']['actions']['listWorkflowRuns']
>['data']['workflow_runs'][number];
type Profile = PullRequest['user'];
type Repository = PullRequest['base']['repo'];
export { PullRequest, Profile, Repository, WorkflowRun, Commit };

View File

@@ -0,0 +1,19 @@
import { useGithub } from '.';
const withGithub = <TProps extends object>(
Component: React.ComponentType<TProps>,
Fallback: React.ComponentType<object>,
) => {
const WrappedComponent: React.FC<TProps> = (props) => {
const github = useGithub();
if (!github.client) {
return <Fallback />;
}
return <Component {...props} />;
};
return WrappedComponent;
};
export { withGithub };

View File

@@ -0,0 +1,17 @@
export {
useGithub,
useGithubQuery,
withGithub,
GithubTypes,
GithubLogin,
} from './github';
export { useLinear, useLinearQuery, withLinear, LinearLogin } from './linear';
export {
useSlack,
useSlackQuery,
withSlack,
SlackClient,
SlackTypes,
SlackLogin,
} from './slack';
export { ClientProvider } from './provider';

View File

@@ -0,0 +1,73 @@
import { LinearClient } from '@linear/sdk';
import { createContext, useCallback, useMemo, useState } from 'react';
type LinearLogin = React.ComponentType<{
setApiKey: (apiKey: string) => void;
cancel: () => void;
}>;
type LinearClientContextValue = {
client?: LinearClient;
logout: () => void;
login?: () => void;
};
type LinearClientProviderProps = {
children: React.ReactNode;
login: LinearLogin;
};
const LinearClientContext = createContext<LinearClientContextValue | null>(
null,
);
const LinearClientProvider: React.FC<LinearClientProviderProps> = ({
children,
login: LinearLoginComponent,
}) => {
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [apiKey, setApiKey] = useState(
localStorage.getItem('linear_token') || '',
);
const client = useMemo(() => {
if (!apiKey) {
return;
}
localStorage.setItem('linear_token', apiKey);
return new LinearClient({ apiKey: apiKey });
}, [apiKey]);
const logout = useCallback(() => {
setApiKey('');
localStorage.removeItem('linear_token');
}, [setApiKey]);
const login = useCallback(() => {
setIsLoggingIn(true);
}, [setIsLoggingIn]);
const onLogin = useCallback(
(nextApiKey: string) => {
setApiKey(nextApiKey);
setIsLoggingIn(false);
},
[setApiKey, setIsLoggingIn],
);
const cancelLogin = useCallback(() => {
setIsLoggingIn(false);
}, [setIsLoggingIn]);
return (
<LinearClientContext.Provider value={{ client, login, logout }}>
{isLoggingIn && (
<LinearLoginComponent setApiKey={onLogin} cancel={cancelLogin} />
)}
{children}
</LinearClientContext.Provider>
);
};
export type { LinearLogin };
export { LinearClientContext, LinearClientProvider };

View File

@@ -0,0 +1,61 @@
import { useCallback, useContext, useMemo, useState } from 'react';
import { LinearClientContext } from './context';
import { LinearClient } from '@linear/sdk';
const useLinear = () => {
const context = useContext(LinearClientContext);
if (!context) {
throw new Error(
'useLinearClient must be used within a LinearClientProvider',
);
}
const value = useMemo(
() => ({
client: context.client,
logout: context.logout,
login: context.login,
}),
[context.client, context.logout, context.login],
);
return value;
};
const useLinearQuery = <T = unknown>(
query: (client: LinearClient) => Promise<T>,
) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<unknown | null>(null);
const [data, setData] = useState<T | null>(null);
const { client } = useLinear();
const fetch = useCallback(async () => {
if (!client) {
throw new Error('Linear client is not initialized');
}
try {
setLoading(true);
const data = await query(client);
setData(data);
return data;
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
}, [client, query]);
const value = useMemo(
() => ({
loading,
error,
data,
fetch,
}),
[loading, error, data, fetch],
);
return value;
};
export { useLinear, useLinearQuery };

View File

@@ -0,0 +1,3 @@
export { LinearClientProvider, LinearLogin } from './context';
export { useLinear, useLinearQuery } from './hooks';
export { withLinear } from './with-linear';

View File

@@ -0,0 +1,19 @@
import { useLinear } from '.';
const withLinear = <TProps extends object>(
Component: React.ComponentType<TProps>,
FallBack: React.ComponentType<object>,
) => {
const WrappedComponent: React.FC<TProps> = (props) => {
const linear = useLinear();
if (!linear.client) {
return <FallBack />;
}
return <Component {...props} />;
};
return WrappedComponent;
};
export { withLinear };

View File

@@ -0,0 +1,29 @@
import { GithubClientProvider, GithubLogin } from './github';
import { LinearClientProvider, LinearLogin } from './linear';
import { SlackClientProvider, SlackLogin } from './slack';
type ClientProviderProps = {
children: React.ReactNode;
logins: {
github: GithubLogin;
linear: LinearLogin;
slack: SlackLogin;
};
};
const ClientProvider: React.FC<ClientProviderProps> = ({
children,
logins,
}) => {
return (
<LinearClientProvider login={logins.linear}>
<GithubClientProvider login={logins.github}>
<SlackClientProvider login={logins.slack}>
{children}
</SlackClientProvider>
</GithubClientProvider>
</LinearClientProvider>
);
};
export { ClientProvider };

View File

@@ -0,0 +1,59 @@
import type { WebClient } from '@slack/web-api';
import type { Expand } from '../../utils/types';
type MethodFromString<T extends `${string}.${string}`> =
T extends `${infer A}.${infer B}`
? A extends keyof WebClient
? B extends keyof WebClient[A]
? WebClient[A][B] extends (...args: any) => any
? (
...args: Parameters<WebClient[A][B]>
) => ReturnType<WebClient[A][B]>
: never
: never
: never
: never;
type ParamsFromString<T extends `${string}.${string}`> =
MethodFromString<T> extends (arg: infer P) => unknown
? P extends Record<string, any>
? P
: never
: never;
type ReturnFromString<T extends `${string}.${string}`> =
MethodFromString<T> extends () => infer R
? R extends Promise<infer P>
? P
: never
: never;
class SlackClient {
#token: string;
constructor(token: string) {
this.#token = token;
}
public send = async <K extends `${string}.${string}`>(
action: K,
params: Expand<ParamsFromString<K>>,
): Promise<Expand<ReturnFromString<K>>> => {
const form = new FormData();
form.append('token', this.#token);
Object.keys(params).forEach((key) => {
form.append(key, params[key as keyof typeof params]);
});
const response = await fetch(`https://slack.com/api/${action}`, {
mode: 'cors',
method: 'POST',
body: form,
});
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText} ${action}`);
}
return response.json();
};
}
export type { ReturnFromString };
export { SlackClient };

View File

@@ -0,0 +1,69 @@
import { createContext, useCallback, useMemo, useState } from 'react';
import { SlackClient } from './client';
type SlackLogin = React.ComponentType<{
setToken: (token: string) => void;
cancel: () => void;
}>;
type SlackClientContextValue = {
client?: SlackClient;
logout: () => void;
login: () => void;
};
type SlackClientProviderProps = {
children: React.ReactNode;
login: SlackLogin;
};
const SlackClientContext = createContext<SlackClientContextValue | null>(null);
const SlackClientProvider: React.FC<SlackClientProviderProps> = ({
children,
login: SlackLoginComponent,
}) => {
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [token, setToken] = useState(localStorage.getItem('slack_token') || '');
const client = useMemo(() => {
if (!token) {
return;
}
localStorage.setItem('slack_token', token);
return new SlackClient(token);
}, [token]);
const logout = useCallback(() => {
setToken('');
localStorage.removeItem('slack_token');
}, [setToken]);
const login = useCallback(() => {
setIsLoggingIn(true);
}, [setIsLoggingIn]);
const onLogin = useCallback(
(nextToken: string) => {
setToken(nextToken);
setIsLoggingIn(false);
},
[setToken, setIsLoggingIn],
);
const cancelLogin = useCallback(() => {
setIsLoggingIn(false);
}, [setIsLoggingIn]);
return (
<SlackClientContext.Provider value={{ client, login, logout }}>
{isLoggingIn && (
<SlackLoginComponent setToken={onLogin} cancel={cancelLogin} />
)}
{children}
</SlackClientContext.Provider>
);
};
export type { SlackLogin };
export { SlackClientContext, SlackClientProvider };

View File

@@ -0,0 +1,62 @@
import { useCallback, useContext, useMemo, useState } from 'react';
import { SlackClientContext } from './context';
import { SlackClient } from './client';
const useSlack = () => {
const context = useContext(SlackClientContext);
if (!context) {
throw new Error('useSlack must be used within a SlackClientProvider');
}
const value = useMemo(
() => ({
client: context.client,
logout: context.logout,
login: context.login,
}),
[context.client, context.logout, context.login],
);
return value;
};
const useSlackQuery = <P, T = unknown>(
query: (client: SlackClient, params: P) => Promise<T>,
) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<unknown | null>(null);
const [data, setData] = useState<T | null>(null);
const { client } = useSlack();
const fetch = useCallback(
async (params: P) => {
if (!client) {
throw new Error('Slack client is not initialized');
}
try {
setLoading(true);
const data = await query(client, params);
setData(data);
return data;
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
},
[client, query],
);
const value = useMemo(
() => ({
loading,
error,
data,
fetch,
}),
[loading, error, data, fetch],
);
return value;
};
export { useSlack, useSlackQuery };

View File

@@ -0,0 +1,5 @@
export { SlackClientProvider, SlackLogin } from './context';
export { SlackClient } from './client';
export { useSlack, useSlackQuery } from './hooks';
export { withSlack } from './with-slack';
export * as SlackTypes from './types';

View File

@@ -0,0 +1,5 @@
import { ReturnFromString } from './client';
type Profile = Exclude<ReturnFromString<'users.info'>['user'], undefined>;
export type { Profile };

View File

@@ -0,0 +1,19 @@
import { useSlack } from '.';
const withSlack = <TProps extends object>(
Component: React.ComponentType<TProps>,
Fallback: React.ComponentType<object>,
) => {
const WrappedComponent: React.FC<TProps> = (props) => {
const slack = useSlack();
if (!slack.client) {
return <Fallback />;
}
return <Component {...props} />;
};
return WrappedComponent;
};
export { withSlack };

View File

@@ -0,0 +1,42 @@
import { useCallback, useEffect, useRef } from 'react';
type AutoUpdateOptions<TReturn> = {
interval: number;
action: () => Promise<TReturn>;
callback?: (next: TReturn, prev?: TReturn) => void;
};
const useAutoUpdate = <T>(
{ interval, action, callback = () => {} }: AutoUpdateOptions<T>,
deps: any[],
) => {
const prev = useRef<T>();
const actionWithCallback = useCallback(action, [...deps]);
const callbackWithCallback = useCallback(callback, [...deps]);
const update = useCallback(async () => {
const next = await actionWithCallback();
callbackWithCallback(next, prev.current);
prev.current = next;
}, [actionWithCallback, callbackWithCallback]);
useEffect(() => {
let intervalId: NodeJS.Timeout;
const update = async () => {
const next = await actionWithCallback();
callbackWithCallback(next, prev.current);
prev.current = next;
intervalId = setTimeout(update, interval);
};
update();
return () => {
clearTimeout(intervalId);
};
}, [interval, actionWithCallback, callbackWithCallback]);
return update;
};
export { useAutoUpdate };

View File

@@ -0,0 +1,6 @@
export * from './clients';
export * from './widgets';
export * from './provider';
export * from './notifications';
export * from './hooks';
export * from './boards';

View File

@@ -0,0 +1,65 @@
import { createContext, useCallback, useMemo, useState } from 'react';
import type { Notification } from './types';
type NotificationsContextValue = {
notifications: Notification[];
add: (notification: Notification) => void;
dismiss: (id: string) => void;
};
type NotificationsProviderProps = {
children: React.ReactNode;
};
const NotificationsContext = createContext<NotificationsContextValue | null>(
null,
);
let nextId = 0;
const NotificationsProvider: React.FC<NotificationsProviderProps> = ({
children,
}) => {
const [notifications, setNotifications] = useState<Notification[]>([]);
const add = useCallback((notification: Notification) => {
setNotifications((current) => [
...current,
{
id: String(nextId++),
...notification,
},
]);
if ('Notification' in window) {
const notify = async () => {
if (Notification.permission !== 'granted') {
await Notification.requestPermission();
}
if (Notification.permission === 'granted') {
const n = new Notification(notification.title || 'Notification', {
body: notification.message,
});
setTimeout(() => n.close(), 10 * 1000);
}
};
notify();
}
}, []);
const dismiss = useCallback((id: string) => {
setNotifications((current) => current.filter((n) => n.id !== id));
}, []);
const value = useMemo(
() => ({ notifications, add, dismiss }),
[notifications, add, dismiss],
);
return (
<NotificationsContext.Provider value={value}>
{children}
</NotificationsContext.Provider>
);
};
export { NotificationsContext, NotificationsProvider };

View File

@@ -0,0 +1,37 @@
import { useContext } from 'react';
import { NotificationsContext } from './context';
const useNotifications = () => {
const context = useContext(NotificationsContext);
if (!context) {
throw new Error(
'useNotifications must be used within a NotificationsProvider',
);
}
return context.notifications;
};
const useNotificationAdd = () => {
const context = useContext(NotificationsContext);
if (!context) {
throw new Error(
'useNotificationAdd must be used within a NotificationsProvider',
);
}
return context.add;
};
const useNotificationDismiss = () => {
const context = useContext(NotificationsContext);
if (!context) {
throw new Error(
'useNotificationDismiss must be used within a NotificationsProvider',
);
}
return context.dismiss;
};
export { useNotifications, useNotificationAdd, useNotificationDismiss };

View File

@@ -0,0 +1,7 @@
export { NotificationsProvider } from './context';
export type { Notification } from './types';
export {
useNotifications,
useNotificationAdd,
useNotificationDismiss,
} from './hooks';

View File

@@ -0,0 +1,12 @@
type Notification = {
view: Symbol;
id?: string;
title?: string;
message: string;
actions?: {
label: string;
callback: () => void;
}[];
};
export type { Notification };

View File

@@ -0,0 +1,40 @@
import { TSchema } from '@sinclair/typebox';
import {
ClientProvider,
GithubLogin,
LinearLogin,
SlackLogin,
} from './clients';
import { Widget, WidgetsProvider } from './widgets';
import { NotificationsProvider } from './notifications';
import { BoardsLoad, BoardsProvider, BoardsSave } from './boards';
type DashboardProviderProps = {
children: React.ReactNode;
widgets?: Widget<TSchema>[];
load: BoardsLoad;
save: BoardsSave;
logins: {
github: GithubLogin;
linear: LinearLogin;
slack: SlackLogin;
};
};
const DashboardProvider: React.FC<DashboardProviderProps> = ({
children,
widgets,
load,
save,
logins,
}) => (
<WidgetsProvider widgets={widgets}>
<BoardsProvider load={load} save={save}>
<NotificationsProvider>
<ClientProvider logins={logins}>{children}</ClientProvider>
</NotificationsProvider>
</BoardsProvider>
</WidgetsProvider>
);
export { DashboardProvider };

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,29 @@
import { TSchema } from '@sinclair/typebox';
import { createContext, useState } from 'react';
import { Widget } from './types';
type WidgetsContextValue = {
widgets: Widget<TSchema>[];
};
type WidgetsProviderProps = {
children: React.ReactNode;
widgets?: Widget<TSchema>[];
};
const WidgetsContext = createContext<WidgetsContextValue | null>(null);
const WidgetsProvider: React.FC<WidgetsProviderProps> = ({
children,
widgets: initialWidgets,
}) => {
const [widgets] = useState<Widget<TSchema>[]>(initialWidgets || []);
return (
<WidgetsContext.Provider value={{ widgets }}>
{children}
</WidgetsContext.Provider>
);
};
export { WidgetsContext, WidgetsProvider };

View File

@@ -0,0 +1,26 @@
import { useWidget } from '.';
import { useWidgetData, useWidgetId } from './hooks';
type WidgetRenderProps = {
onSave: (data: any) => void;
};
const WidgetEditor: React.FC<WidgetRenderProps> = ({ onSave }) => {
const id = useWidgetId();
const data = useWidgetData();
const widget = useWidget(id);
if (!widget) {
return null;
}
const Component = widget.edit;
if (!Component) {
return null;
}
return <Component value={data} save={onSave} />;
};
export { WidgetEditor };

View File

@@ -0,0 +1,124 @@
import { useCallback, useContext, useMemo, useState } from 'react';
import { WidgetsContext } from './context';
import { WidgetContext } from './widget-context';
const useWidget = (id: string) => {
const context = useContext(WidgetsContext);
if (!context) {
throw new Error('useWidget must be used within a WidgetsProvider');
}
const current = useMemo(() => {
return context.widgets.find((widget) => widget.id === id);
}, [context.widgets, id]);
return current;
};
const useWidgets = () => {
const context = useContext(WidgetsContext);
if (!context) {
throw new Error('useWidgets must be used within a WidgetsProvider');
}
return context.widgets;
};
type WidgetResult = {
id: string;
data: any;
description?: string;
name: string;
icon?: React.ReactNode;
};
const useGetWidgetsFromUrl = () => {
const [current, setCurrent] = useState<WidgetResult[]>([]);
const widgets = useWidgets();
const update = useCallback(
(url: URL) => {
const result = widgets.map((widget) => {
const parsed = widget.parseUrl?.(url);
if (!parsed) {
return null;
}
return {
id: widget.id,
name: widget.name,
description: widget.description,
icon: widget.icon,
data: parsed,
};
});
setCurrent(result.filter(Boolean) as WidgetResult[]);
},
[widgets],
);
return [current, update] as const;
};
const useDismissWidgetNotification = () => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error(
'useDismissWidgetNotification must be used within a WidgetProvider',
);
}
return context.dismissNotification;
};
const useAddWidgetNotification = () => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error(
'useAddWidgetNotification must be used within a WidgetProvider',
);
}
return context.addNotification;
};
const useWidgetNotifications = () => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error(
'useWidgetNotifications must be used within a WidgetProvider',
);
}
return context.notifications;
};
const useWidgetData = () => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error('useWidgetData must be used within a WidgetProvider');
}
return context.data;
};
const useSetWidgetData = () => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error('useSetWidgetData must be used within a WidgetProvider');
}
return context.setData;
};
const useWidgetId = () => {
const context = useContext(WidgetContext);
if (!context) {
throw new Error('useWidgetId must be used within a WidgetProvider');
}
return context.id;
};
export {
useWidget,
useWidgets,
useGetWidgetsFromUrl,
useWidgetNotifications,
useDismissWidgetNotification,
useAddWidgetNotification,
useWidgetData,
useSetWidgetData,
useWidgetId,
};

View File

@@ -0,0 +1,16 @@
export { WidgetsProvider } from './context';
export type { Widget } from './types';
export {
useWidget,
useWidgets,
useGetWidgetsFromUrl,
useAddWidgetNotification,
useDismissWidgetNotification,
useWidgetNotifications,
useWidgetId,
useWidgetData,
useSetWidgetData,
} from './hooks';
export { WidgetProvider } from './widget-context';
export { WidgetView } from './view';
export { WidgetEditor } from './editor';

View File

@@ -0,0 +1,17 @@
import { TSchema, Static } from '@sinclair/typebox';
type Widget<TConfig extends TSchema> = {
name: string;
id: string;
description?: string;
icon?: React.ReactNode;
schema: TSchema;
parseUrl?: (url: URL) => Static<TConfig> | undefined;
component: React.ComponentType<Static<TConfig>>;
edit?: React.ComponentType<{
value?: Static<TConfig>;
save: (next: Static<TConfig>) => void;
}>;
};
export type { Widget };

View File

@@ -0,0 +1,18 @@
import { useWidget } from '.';
import { useWidgetData, useWidgetId } from './hooks';
const WidgetView: React.FC = () => {
const id = useWidgetId();
const data = useWidgetData();
const widget = useWidget(id);
if (!widget) {
return null;
}
const Component = widget.component;
return <Component {...data} />;
};
export { WidgetView };

View File

@@ -0,0 +1,73 @@
import { createContext, useCallback, useMemo, useRef } from 'react';
import {
Notification as BaseNotification,
useNotificationAdd,
useNotifications,
useNotificationDismiss,
} from '../notifications';
type Notification = Omit<BaseNotification, 'view'>;
type WidgetContextValue = {
id: string;
data?: any;
addNotification: (notification: Notification) => void;
dismissNotification: (id: string) => void;
notifications: Notification[];
setData?: (data: any) => void;
};
type WidgetProviderProps = {
id: string;
data?: any;
setData?: (data: any) => void;
children: React.ReactNode;
};
const WidgetContext = createContext<WidgetContextValue | null>(null);
const WidgetProvider = ({
id,
data,
setData,
children,
}: WidgetProviderProps) => {
const ref = useRef(Symbol('WidgetRender'));
const globalNotifications = useNotifications();
const addGlobalNotification = useNotificationAdd();
const dissmissGlobalNotification = useNotificationDismiss();
const notifications = useMemo(() => {
return globalNotifications.filter((n) => n.view !== ref.current);
}, [globalNotifications]);
const addNotification = useCallback(
(notification: Notification) => {
addGlobalNotification({ ...notification, view: ref.current });
},
[addGlobalNotification],
);
const dismissNotification = useCallback(
(dismissId: string) => {
dissmissGlobalNotification(dismissId);
},
[dissmissGlobalNotification],
);
const value = useMemo(
() => ({
addNotification,
dismissNotification,
notifications,
id,
data,
setData,
}),
[addNotification, notifications, id, data, setData, dismissNotification],
);
return (
<WidgetContext.Provider value={value}>{children}</WidgetContext.Provider>
);
};
export { WidgetContext, WidgetProvider };

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"declarationDir": "./dist/esm/types",
"outDir": "dist/esm"
},
"extends": "@refocus/config/esm",
"include": [
"src/**/*"
]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"declarationDir": "./dist/cjs/types",
"outDir": "dist/cjs"
},
"extends": "@refocus/config/cjs",
"include": [
"src/**/*"
]
}

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

Some files were not shown because too many files have changed in this diff Show More