mirror of
https://github.com/morten-olsen/refocus.dev.git
synced 2026-02-08 00:46:25 +01:00
init
This commit is contained in:
14
packages/app/.eslintrc.cjs
Normal file
14
packages/app/.eslintrc.cjs
Normal 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
24
packages/app/.gitignore
vendored
Normal 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
20
packages/app/index.html
Normal 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
45
packages/app/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
packages/app/public/vite.svg
Normal file
1
packages/app/public/vite.svg
Normal 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
12
packages/app/src/app.tsx
Normal 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;
|
||||
9
packages/app/src/main.tsx
Normal file
9
packages/app/src/main.tsx
Normal 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
1
packages/app/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
25
packages/app/tsconfig.json
Normal file
25
packages/app/tsconfig.json
Normal 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" }]
|
||||
}
|
||||
10
packages/app/tsconfig.node.json
Normal file
10
packages/app/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
28
packages/app/vite.config.ts
Normal file
28
packages/app/vite.config.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
9
packages/configs/cjs/tsconfig.json
Normal file
9
packages/configs/cjs/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2019", "DOM"],
|
||||
"target": "ES2019",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node"
|
||||
}
|
||||
}
|
||||
9
packages/configs/esm/tsconfig.json
Normal file
9
packages/configs/esm/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
}
|
||||
}
|
||||
4
packages/configs/package.json
Normal file
4
packages/configs/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@refocus/config",
|
||||
"version": "0.0.1"
|
||||
}
|
||||
19
packages/configs/tsconfig.json
Normal file
19
packages/configs/tsconfig.json
Normal 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
2
packages/sdk/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
38
packages/sdk/package.json
Normal file
38
packages/sdk/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
140
packages/sdk/src/boards/context.tsx
Normal file
140
packages/sdk/src/boards/context.tsx
Normal 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 };
|
||||
77
packages/sdk/src/boards/hooks.ts
Normal file
77
packages/sdk/src/boards/hooks.ts
Normal 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,
|
||||
};
|
||||
12
packages/sdk/src/boards/index.ts
Normal file
12
packages/sdk/src/boards/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { BoardsProvider } from './context';
|
||||
export {
|
||||
useBoards,
|
||||
useSelectedBoard,
|
||||
useAddWidget,
|
||||
useRemoveWidget,
|
||||
useAddBoard,
|
||||
useRemoveBoard,
|
||||
useSelectBoard,
|
||||
useUpdateWidget,
|
||||
} from './hooks';
|
||||
export * from './types';
|
||||
23
packages/sdk/src/boards/types.ts
Normal file
23
packages/sdk/src/boards/types.ts
Normal 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 };
|
||||
71
packages/sdk/src/clients/github/context.tsx
Normal file
71
packages/sdk/src/clients/github/context.tsx
Normal 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 };
|
||||
64
packages/sdk/src/clients/github/hooks.ts
Normal file
64
packages/sdk/src/clients/github/hooks.ts
Normal 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 };
|
||||
4
packages/sdk/src/clients/github/index.ts
Normal file
4
packages/sdk/src/clients/github/index.ts
Normal 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';
|
||||
12
packages/sdk/src/clients/github/types.ts
Normal file
12
packages/sdk/src/clients/github/types.ts
Normal 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 };
|
||||
19
packages/sdk/src/clients/github/with-github.tsx
Normal file
19
packages/sdk/src/clients/github/with-github.tsx
Normal 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 };
|
||||
17
packages/sdk/src/clients/index.ts
Normal file
17
packages/sdk/src/clients/index.ts
Normal 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';
|
||||
73
packages/sdk/src/clients/linear/context.tsx
Normal file
73
packages/sdk/src/clients/linear/context.tsx
Normal 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 };
|
||||
61
packages/sdk/src/clients/linear/hooks.ts
Normal file
61
packages/sdk/src/clients/linear/hooks.ts
Normal 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 };
|
||||
3
packages/sdk/src/clients/linear/index.ts
Normal file
3
packages/sdk/src/clients/linear/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { LinearClientProvider, LinearLogin } from './context';
|
||||
export { useLinear, useLinearQuery } from './hooks';
|
||||
export { withLinear } from './with-linear';
|
||||
19
packages/sdk/src/clients/linear/with-linear.tsx
Normal file
19
packages/sdk/src/clients/linear/with-linear.tsx
Normal 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 };
|
||||
29
packages/sdk/src/clients/provider.tsx
Normal file
29
packages/sdk/src/clients/provider.tsx
Normal 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 };
|
||||
59
packages/sdk/src/clients/slack/client.ts
Normal file
59
packages/sdk/src/clients/slack/client.ts
Normal 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 };
|
||||
69
packages/sdk/src/clients/slack/context.tsx
Normal file
69
packages/sdk/src/clients/slack/context.tsx
Normal 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 };
|
||||
62
packages/sdk/src/clients/slack/hooks.ts
Normal file
62
packages/sdk/src/clients/slack/hooks.ts
Normal 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 };
|
||||
5
packages/sdk/src/clients/slack/index.ts
Normal file
5
packages/sdk/src/clients/slack/index.ts
Normal 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';
|
||||
5
packages/sdk/src/clients/slack/types.ts
Normal file
5
packages/sdk/src/clients/slack/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ReturnFromString } from './client';
|
||||
|
||||
type Profile = Exclude<ReturnFromString<'users.info'>['user'], undefined>;
|
||||
|
||||
export type { Profile };
|
||||
19
packages/sdk/src/clients/slack/with-slack.tsx
Normal file
19
packages/sdk/src/clients/slack/with-slack.tsx
Normal 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 };
|
||||
42
packages/sdk/src/hooks/index.ts
Normal file
42
packages/sdk/src/hooks/index.ts
Normal 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 };
|
||||
6
packages/sdk/src/index.ts
Normal file
6
packages/sdk/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './clients';
|
||||
export * from './widgets';
|
||||
export * from './provider';
|
||||
export * from './notifications';
|
||||
export * from './hooks';
|
||||
export * from './boards';
|
||||
65
packages/sdk/src/notifications/context.tsx
Normal file
65
packages/sdk/src/notifications/context.tsx
Normal 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 };
|
||||
37
packages/sdk/src/notifications/hooks.ts
Normal file
37
packages/sdk/src/notifications/hooks.ts
Normal 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 };
|
||||
7
packages/sdk/src/notifications/index.ts
Normal file
7
packages/sdk/src/notifications/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { NotificationsProvider } from './context';
|
||||
export type { Notification } from './types';
|
||||
export {
|
||||
useNotifications,
|
||||
useNotificationAdd,
|
||||
useNotificationDismiss,
|
||||
} from './hooks';
|
||||
12
packages/sdk/src/notifications/types.ts
Normal file
12
packages/sdk/src/notifications/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
type Notification = {
|
||||
view: Symbol;
|
||||
id?: string;
|
||||
title?: string;
|
||||
message: string;
|
||||
actions?: {
|
||||
label: string;
|
||||
callback: () => void;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type { Notification };
|
||||
40
packages/sdk/src/provider.tsx
Normal file
40
packages/sdk/src/provider.tsx
Normal 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 };
|
||||
16
packages/sdk/src/utils/types.ts
Normal file
16
packages/sdk/src/utils/types.ts
Normal 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 };
|
||||
29
packages/sdk/src/widgets/context.tsx
Normal file
29
packages/sdk/src/widgets/context.tsx
Normal 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 };
|
||||
26
packages/sdk/src/widgets/editor.tsx
Normal file
26
packages/sdk/src/widgets/editor.tsx
Normal 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 };
|
||||
124
packages/sdk/src/widgets/hooks.ts
Normal file
124
packages/sdk/src/widgets/hooks.ts
Normal 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,
|
||||
};
|
||||
16
packages/sdk/src/widgets/index.ts
Normal file
16
packages/sdk/src/widgets/index.ts
Normal 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';
|
||||
17
packages/sdk/src/widgets/types.ts
Normal file
17
packages/sdk/src/widgets/types.ts
Normal 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 };
|
||||
18
packages/sdk/src/widgets/view.tsx
Normal file
18
packages/sdk/src/widgets/view.tsx
Normal 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 };
|
||||
73
packages/sdk/src/widgets/widget-context.tsx
Normal file
73
packages/sdk/src/widgets/widget-context.tsx
Normal 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 };
|
||||
10
packages/sdk/tsconfig.esm.json
Normal file
10
packages/sdk/tsconfig.esm.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declarationDir": "./dist/esm/types",
|
||||
"outDir": "dist/esm"
|
||||
},
|
||||
"extends": "@refocus/config/esm",
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
10
packages/sdk/tsconfig.json
Normal file
10
packages/sdk/tsconfig.json
Normal 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
2
packages/ui/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
17
packages/ui/.storybook/main.ts
Normal file
17
packages/ui/.storybook/main.ts
Normal 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;
|
||||
24
packages/ui/.storybook/preview.tsx
Normal file
24
packages/ui/.storybook/preview.tsx
Normal 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
53
packages/ui/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
84
packages/ui/src/base/avatar/index.tsx
Normal file
84
packages/ui/src/base/avatar/index.tsx
Normal 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 };
|
||||
30
packages/ui/src/base/button/index.tsx
Normal file
30
packages/ui/src/base/button/index.tsx
Normal 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 };
|
||||
24
packages/ui/src/base/card/index.tsx
Normal file
24
packages/ui/src/base/card/index.tsx
Normal 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 };
|
||||
30
packages/ui/src/base/dialog/index.stories.tsx
Normal file
30
packages/ui/src/base/dialog/index.stories.tsx
Normal 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 };
|
||||
102
packages/ui/src/base/dialog/index.tsx
Normal file
102
packages/ui/src/base/dialog/index.tsx
Normal 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 };
|
||||
43
packages/ui/src/base/dropdown/index.stories.tsx
Normal file
43
packages/ui/src/base/dropdown/index.stories.tsx
Normal 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 };
|
||||
162
packages/ui/src/base/dropdown/index.tsx
Normal file
162
packages/ui/src/base/dropdown/index.tsx
Normal 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 };
|
||||
9
packages/ui/src/base/index.ts
Normal file
9
packages/ui/src/base/index.ts
Normal 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';
|
||||
15
packages/ui/src/base/list/index.tsx
Normal file
15
packages/ui/src/base/list/index.tsx
Normal 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 };
|
||||
11
packages/ui/src/base/panel/index.module.scss
Normal file
11
packages/ui/src/base/panel/index.module.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.root {
|
||||
@apply bg-white rounded-lg shadow-lg;
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply text-2xl font-bold;
|
||||
}
|
||||
|
||||
.content {
|
||||
@apply p-4;
|
||||
}
|
||||
16
packages/ui/src/base/panel/index.tsx
Normal file
16
packages/ui/src/base/panel/index.tsx
Normal 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 };
|
||||
13
packages/ui/src/base/row/index.tsx
Normal file
13
packages/ui/src/base/row/index.tsx
Normal 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 };
|
||||
68
packages/ui/src/base/tabs/index.tsx
Normal file
68
packages/ui/src/base/tabs/index.tsx
Normal 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 };
|
||||
91
packages/ui/src/base/view/index.tsx
Normal file
91
packages/ui/src/base/view/index.tsx
Normal 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 };
|
||||
35
packages/ui/src/chat/compose/index.tsx
Normal file
35
packages/ui/src/chat/compose/index.tsx
Normal 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 };
|
||||
2
packages/ui/src/chat/index.ts
Normal file
2
packages/ui/src/chat/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './message';
|
||||
export * from './compose';
|
||||
26
packages/ui/src/chat/message/index.stories.tsx
Normal file
26
packages/ui/src/chat/message/index.stories.tsx
Normal 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;
|
||||
71
packages/ui/src/chat/message/index.tsx
Normal file
71
packages/ui/src/chat/message/index.tsx
Normal 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 };
|
||||
237
packages/ui/src/github/action/data.json
Normal file
237
packages/ui/src/github/action/data.json
Normal 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"
|
||||
}
|
||||
}
|
||||
20
packages/ui/src/github/action/index.stories.tsx
Normal file
20
packages/ui/src/github/action/index.stories.tsx
Normal 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;
|
||||
56
packages/ui/src/github/action/index.tsx
Normal file
56
packages/ui/src/github/action/index.tsx
Normal 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 };
|
||||
5
packages/ui/src/github/index.ts
Normal file
5
packages/ui/src/github/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './notification';
|
||||
export * from './profile';
|
||||
export * from './pull-request';
|
||||
export * from './login';
|
||||
export * from './not-logged-in';
|
||||
35
packages/ui/src/github/login/index.tsx
Normal file
35
packages/ui/src/github/login/index.tsx
Normal 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 };
|
||||
26
packages/ui/src/github/not-logged-in/index.tsx
Normal file
26
packages/ui/src/github/not-logged-in/index.tsx
Normal 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 };
|
||||
22
packages/ui/src/github/notification/index.tsx
Normal file
22
packages/ui/src/github/notification/index.tsx
Normal 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 };
|
||||
45
packages/ui/src/github/profile/index.stories.tsx
Normal file
45
packages/ui/src/github/profile/index.stories.tsx
Normal 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;
|
||||
33
packages/ui/src/github/profile/index.tsx
Normal file
33
packages/ui/src/github/profile/index.tsx
Normal 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 };
|
||||
536
packages/ui/src/github/pull-request/data.json
Normal file
536
packages/ui/src/github/pull-request/data.json
Normal 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
|
||||
}
|
||||
20
packages/ui/src/github/pull-request/index.stories.tsx
Normal file
20
packages/ui/src/github/pull-request/index.stories.tsx
Normal 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;
|
||||
32
packages/ui/src/github/pull-request/index.tsx
Normal file
32
packages/ui/src/github/pull-request/index.tsx
Normal 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
9
packages/ui/src/index.ts
Normal 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';
|
||||
84
packages/ui/src/interface/add-from-url/index.tsx
Normal file
84
packages/ui/src/interface/add-from-url/index.tsx
Normal 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 };
|
||||
66
packages/ui/src/interface/app/index.tsx
Normal file
66
packages/ui/src/interface/app/index.tsx
Normal 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 };
|
||||
62
packages/ui/src/interface/board/index.tsx
Normal file
62
packages/ui/src/interface/board/index.tsx
Normal 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 };
|
||||
81
packages/ui/src/interface/create-widget/index.tsx
Normal file
81
packages/ui/src/interface/create-widget/index.tsx
Normal 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 };
|
||||
6
packages/ui/src/interface/index.ts
Normal file
6
packages/ui/src/interface/index.ts
Normal 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';
|
||||
26
packages/ui/src/interface/notifications/index.tsx
Normal file
26
packages/ui/src/interface/notifications/index.tsx
Normal 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 };
|
||||
96
packages/ui/src/interface/widget/index.tsx
Normal file
96
packages/ui/src/interface/widget/index.tsx
Normal 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 };
|
||||
4
packages/ui/src/linear/index.ts
Normal file
4
packages/ui/src/linear/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './issue';
|
||||
export * from './notification';
|
||||
export * from './login';
|
||||
export * from './not-logged-in';
|
||||
19
packages/ui/src/linear/issue/index.tsx
Normal file
19
packages/ui/src/linear/issue/index.tsx
Normal 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 };
|
||||
35
packages/ui/src/linear/login/index.tsx
Normal file
35
packages/ui/src/linear/login/index.tsx
Normal 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
Reference in New Issue
Block a user