mirror of
https://github.com/urbit/shrub.git
synced 2024-11-23 20:26:54 +03:00
grid: init combination from liam + hunter
This commit is contained in:
parent
70bba97180
commit
f80cf2668a
@ -2,7 +2,8 @@
|
||||
"packages": [
|
||||
"pkg/npm/*",
|
||||
"pkg/btc-wallet",
|
||||
"pkg/interface"
|
||||
"pkg/interface",
|
||||
"pkg/grid"
|
||||
],
|
||||
"version": "independent"
|
||||
}
|
||||
|
8531
package-lock.json
generated
8531
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,8 @@
|
||||
"eslint": "^7.29.0",
|
||||
"husky": "^6.0.0",
|
||||
"lerna": "^4.0.0",
|
||||
"lint-staged": "^11.0.0"
|
||||
"lint-staged": "^11.1.2",
|
||||
"prettier": "^2.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
"watch-libs": "lerna run watch --no-private --parallel",
|
||||
|
7
pkg/grid/.eslintignore
Normal file
7
pkg/grid/.eslintignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
bin/
|
||||
.vscode/
|
||||
.husky/
|
||||
*.config.js
|
||||
*.config.ts
|
66
pkg/grid/.eslintrc.js
Normal file
66
pkg/grid/.eslintrc.js
Normal file
@ -0,0 +1,66 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
'airbnb',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:tailwind/recommended',
|
||||
'prettier',
|
||||
'prettier/prettier',
|
||||
'plugin:prettier/recommended'
|
||||
],
|
||||
ignorePatterns: ['**/*.config.js', '**/*.config.ts'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
ecmaVersion: 12,
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: ['react', '@typescript-eslint', 'import', 'jsx-a11y', 'react-hooks'],
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error'],
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'react/jsx-filename-extension': ['warn', { extensions: ['.tsx'] }],
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
ts: 'never',
|
||||
tsx: 'never'
|
||||
}
|
||||
],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': ['error'],
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
'react/require-default-props': 'off',
|
||||
'import/no-extraneous-dependencies': ['error'],
|
||||
'tailwind/class-order': 'off'
|
||||
},
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts']
|
||||
},
|
||||
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
7
pkg/grid/.gitignore
vendored
Normal file
7
pkg/grid/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
stats.html
|
||||
.eslintcache
|
1
pkg/grid/.husky/.gitignore
vendored
Normal file
1
pkg/grid/.husky/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
_
|
6
pkg/grid/.husky/pre-commit
Executable file
6
pkg/grid/.husky/pre-commit
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd pkg/grid
|
||||
npm test
|
||||
npx lint-staged
|
6
pkg/grid/.prettierrc
Normal file
6
pkg/grid/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
16
pkg/grid/index.html
Normal file
16
pkg/grid/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Landscape • Home</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;600&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body class="text-sm font-sans text-gray-900 bg-white antialiased">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
12835
pkg/grid/package-lock.json
generated
Normal file
12835
pkg/grid/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
76
pkg/grid/package.json
Normal file
76
pkg/grid/package.json
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "landscape",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"mock": "vite --mode mock",
|
||||
"build:mock": "tsc && vite build --mode mock",
|
||||
"build:profile": "tsc && vite build --mode profile",
|
||||
"build": "tsc && vite build",
|
||||
"serve": "vite preview",
|
||||
"lint": "eslint --cache \"**/*.{js,jsx,ts,tsx}\"",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"prepare": "husky install",
|
||||
"test": "echo \"No test yet\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^0.0.20",
|
||||
"@radix-ui/react-dropdown-menu": "^0.0.23",
|
||||
"@radix-ui/react-polymorphic": "^0.0.13",
|
||||
"@radix-ui/react-portal": "^0.0.15",
|
||||
"@urbit/http-api": "^1.3.1",
|
||||
"classnames": "^2.3.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"color2k": "^1.2.4",
|
||||
"fuzzy": "^0.1.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"postcss-import": "^14.0.2",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"react-query": "^3.19.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"slugify": "^1.6.0",
|
||||
"zustand": "^3.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/aspect-ratio": "^0.2.1",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/mousetrap": "^1.6.8",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.1",
|
||||
"@typescript-eslint/parser": "^4.26.1",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.1",
|
||||
"autoprefixer": "^10.3.1",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-import-resolver-typescript": "^2.4.0",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-tailwind": "^0.2.1",
|
||||
"husky": "^7.0.0",
|
||||
"lint-staged": "^11.1.2",
|
||||
"postcss": "^8.3.6",
|
||||
"prettier": "^2.3.2",
|
||||
"rollup-plugin-analyzer": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^5.5.2",
|
||||
"tailwindcss": "^2.2.7",
|
||||
"tailwindcss-touch": "^1.0.1",
|
||||
"typescript": "^4.3.2",
|
||||
"vite": "^2.4.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"npm run lint:fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.+{json,css,md}": "prettier --write"
|
||||
}
|
||||
}
|
7
pkg/grid/postcss.config.js
Normal file
7
pkg/grid/postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-import'),
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
]
|
||||
}
|
43
pkg/grid/src/app.tsx
Normal file
43
pkg/grid/src/app.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { BrowserRouter, Switch, Route, useHistory } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Grid } from './pages/Grid';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const AppRoutes = () => {
|
||||
const { push } = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
window.name = 'grid';
|
||||
|
||||
Mousetrap.bind(['command+/', 'ctrl+/'], () => {
|
||||
push('/leap/search');
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={['/leap/:menu', '/']} component={Grid} />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export function App() {
|
||||
const base = import.meta.env.MODE === 'mock' ? undefined : '/apps/grid';
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter basename={base}>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
BIN
pkg/grid/src/assets/go.png
Normal file
BIN
pkg/grid/src/assets/go.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
pkg/grid/src/assets/system.png
Normal file
BIN
pkg/grid/src/assets/system.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
15
pkg/grid/src/components/Attribute.tsx
Normal file
15
pkg/grid/src/components/Attribute.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { capitalize } from 'lodash-es';
|
||||
|
||||
interface AttributeProps {
|
||||
attr: string;
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const Attribute = ({ attr, children, title }: AttributeProps) => (
|
||||
<div className="h4">
|
||||
<h2 className="mb-2 text-gray-500">{title || capitalize(attr)}</h2>
|
||||
<p className="font-mono">{children}</p>
|
||||
</div>
|
||||
);
|
42
pkg/grid/src/components/Button.tsx
Normal file
42
pkg/grid/src/components/Button.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import type * as Polymorphic from '@radix-ui/react-polymorphic';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'destructive';
|
||||
|
||||
type PolymorphicButton = Polymorphic.ForwardRefComponent<
|
||||
'button',
|
||||
{
|
||||
variant?: ButtonVariant;
|
||||
}
|
||||
>;
|
||||
|
||||
const variants: Record<ButtonVariant, string> = {
|
||||
primary: 'text-white bg-blue-400',
|
||||
secondary: 'text-blue-400 bg-blue-100',
|
||||
destructive: 'text-white bg-red-400'
|
||||
};
|
||||
|
||||
export const Button = React.forwardRef(
|
||||
({ as: Comp = 'button', variant = 'primary', children, className, ...props }, ref) => {
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={classNames('button default-ring', variants[variant], className)}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
) as PolymorphicButton;
|
||||
|
||||
export const PillButton = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={classNames('px-4 py-2 sm:px-6 sm:py-3 text-sm sm:text-base rounded-full', className)}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)) as PolymorphicButton;
|
51
pkg/grid/src/components/Dialog.tsx
Normal file
51
pkg/grid/src/components/Dialog.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React, { FC } from 'react';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import type * as Polymorphic from '@radix-ui/react-polymorphic';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const Dialog: FC<DialogPrimitive.DialogOwnProps> = ({ children, ...props }) => {
|
||||
return (
|
||||
<DialogPrimitive.Root {...props}>
|
||||
<DialogPrimitive.Overlay className="fixed top-0 bottom-0 left-0 right-0 z-30 bg-black opacity-30" />
|
||||
{children}
|
||||
</DialogPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
type DialogContentComponent = Polymorphic.ForwardRefComponent<
|
||||
Polymorphic.IntrinsicElement<typeof DialogPrimitive.Content>,
|
||||
Polymorphic.OwnProps<typeof DialogPrimitive.Content> & {
|
||||
containerClass?: string;
|
||||
showClose?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export const DialogContent = React.forwardRef(
|
||||
({ showClose = true, containerClass, children, className, ...props }, forwardedRef) => (
|
||||
<DialogPrimitive.Content
|
||||
as="section"
|
||||
className={classNames('dialog-container', containerClass)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<div className={classNames('dialog', className)}>
|
||||
{children}
|
||||
{showClose && (
|
||||
<DialogPrimitive.Close className="absolute top-4 right-4 sm:top-7 sm:right-7 p-2 bg-gray-100 rounded-full default-ring">
|
||||
<svg
|
||||
className="w-3.5 h-3.5 stroke-current text-gray-500"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M4 4L20 20" strokeWidth="3" strokeLinecap="round" />
|
||||
<path d="M20 4L4 20" strokeWidth="3" strokeLinecap="round" />
|
||||
</svg>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
</DialogPrimitive.Content>
|
||||
)
|
||||
) as DialogContentComponent;
|
||||
|
||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
||||
export const DialogClose = DialogPrimitive.Close;
|
35
pkg/grid/src/components/DocketHeader.tsx
Normal file
35
pkg/grid/src/components/DocketHeader.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Docket } from '../state/docket-types';
|
||||
|
||||
interface DocketHeaderProps {
|
||||
docket: Docket;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function DocketHeader(props: DocketHeaderProps) {
|
||||
const { docket, children } = props;
|
||||
const color = `#${docket.color.slice(2).replace('.', '')}`.toUpperCase();
|
||||
const { info, title, img } = docket;
|
||||
|
||||
return (
|
||||
<header className="grid grid-cols-[5rem,1fr] md:grid-cols-[8rem,1fr] auto-rows-min grid-flow-row-dense mb-5 sm:mb-8 gap-x-6 gap-y-4">
|
||||
<div
|
||||
className="flex-none row-span-1 md:row-span-2 relative w-20 h-20 md:w-32 md:h-32 bg-gray-200 rounded-xl"
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{img && (
|
||||
<img
|
||||
className="absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2"
|
||||
src={img}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-start-2">
|
||||
<h1 className="h2">{title}</h1>
|
||||
{info && <p className="h4 mt-2 text-gray-500">{info}</p>}
|
||||
</div>
|
||||
{children}
|
||||
</header>
|
||||
);
|
||||
}
|
20
pkg/grid/src/components/ShipName.tsx
Normal file
20
pkg/grid/src/components/ShipName.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
|
||||
type ShipNameProps = {
|
||||
name: string;
|
||||
} & HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
export const ShipName = ({ name, ...props }: ShipNameProps) => {
|
||||
const parts = name.replace('~', '').split(/[_^-]/);
|
||||
|
||||
return (
|
||||
<span {...props}>
|
||||
<span aria-hidden>~</span>
|
||||
{/* <span className="sr-only">sig</span> */}
|
||||
<span>{parts[0]}</span>
|
||||
<span aria-hidden>-</span>
|
||||
{/* <span className="sr-only">hep</span> */}
|
||||
<span>{parts[1]}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
7
pkg/grid/src/components/Spinner.tsx
Normal file
7
pkg/grid/src/components/Spinner.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { SpinnerIcon } from './icons/SpinnerIcon';
|
||||
|
||||
export const Spinner = ({ className, ...props }: React.HTMLAttributes<SVGSVGElement>) => (
|
||||
<SpinnerIcon className={classNames('spinner', className)} {...props} />
|
||||
);
|
26
pkg/grid/src/components/TreatyMeta.tsx
Normal file
26
pkg/grid/src/components/TreatyMeta.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Treaty } from '../state/docket-types';
|
||||
|
||||
import { Attribute } from './Attribute';
|
||||
|
||||
const meta = ['license', 'website', 'version'] as const;
|
||||
|
||||
export function TreatyMeta(props: { treaty: Treaty }) {
|
||||
const { treaty } = props;
|
||||
const { desk, ship, cass } = treaty;
|
||||
return (
|
||||
<div className="mt-5 sm:mt-8 space-y-5 sm:space-y-8">
|
||||
<Attribute title="Developer Desk" attr="desk">
|
||||
{ship}/{desk}
|
||||
</Attribute>
|
||||
<Attribute title="Last Software Update" attr="case">
|
||||
{JSON.stringify(cass)}
|
||||
</Attribute>
|
||||
{meta.map((d) => (
|
||||
<Attribute key={d} attr={d}>
|
||||
{treaty[d]}
|
||||
</Attribute>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
11
pkg/grid/src/components/icons/Adjust.tsx
Normal file
11
pkg/grid/src/components/icons/Adjust.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Adjust = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 24 24">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6 2C5.44772 2 5 2.44772 5 3V10.126C3.27477 10.5701 2 12.1362 2 14C2 15.8638 3.27477 17.4299 5 17.874V21C5 21.5523 5.44772 22 6 22C6.55228 22 7 21.5523 7 21V17.874C8.72523 17.4299 10 15.8638 10 14C10 12.1362 8.72523 10.5701 7 10.126V3C7 2.44772 6.55228 2 6 2ZM18 2C17.4477 2 17 2.44772 17 3V6.12602C15.2748 6.57006 14 8.13616 14 10C14 11.8638 15.2748 13.4299 17 13.874V21C17 21.5523 17.4477 22 18 22C18.5523 22 19 21.5523 19 21V13.874C20.7252 13.4299 22 11.8638 22 10C22 8.13616 20.7252 6.57006 19 6.12602V3C19 2.44772 18.5523 2 18 2ZM6 16C7.10457 16 8 15.1046 8 14C8 12.8954 7.10457 12 6 12C4.89543 12 4 12.8954 4 14C4 15.1046 4.89543 16 6 16Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
7
pkg/grid/src/components/icons/Bullet.tsx
Normal file
7
pkg/grid/src/components/icons/Bullet.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Bullet = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 16 16">
|
||||
<circle cx="8" cy="8" r="3" />
|
||||
</svg>
|
||||
);
|
11
pkg/grid/src/components/icons/Cross.tsx
Normal file
11
pkg/grid/src/components/icons/Cross.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Cross = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} viewBox="0 0 12 12">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.292893 10.2942C-0.0976311 10.6847 -0.0976311 11.3179 0.292893 11.7084C0.683417 12.0989 1.31658 12.0989 1.70711 11.7084L6.00063 7.41487L10.2948 11.7091C10.6853 12.0996 11.3185 12.0996 11.709 11.7091C12.0996 11.3185 12.0996 10.6854 11.709 10.2949L7.41484 6.00066L11.7084 1.70711C12.0989 1.31658 12.0989 0.683417 11.7084 0.292894C11.3179 -0.0976312 10.6847 -0.0976312 10.2942 0.292894L6.00063 4.58645L1.70775 0.293571C1.31723 -0.0969534 0.684061 -0.0969534 0.293536 0.293571C-0.0969879 0.684095 -0.0969879 1.31726 0.293536 1.70778L4.58641 6.00066L0.292893 10.2942Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
11
pkg/grid/src/components/icons/SpinnerIcon.tsx
Normal file
11
pkg/grid/src/components/icons/SpinnerIcon.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
export const SpinnerIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} fill="none" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="7" className="stroke-current" strokeOpacity="0.4" strokeWidth="8" />
|
||||
<path
|
||||
className="fill-current"
|
||||
d="M23 12C23 13.7359 22.5892 15.4472 21.8011 16.9939C21.013 18.5406 19.87 19.8788 18.4656 20.8992C17.0613 21.9195 15.4353 22.593 13.7208 22.8646C12.0062 23.1361 10.2518 22.998 8.60081 22.4616L11.0482 14.9293C11.5105 15.0795 12.0017 15.1181 12.4818 15.0421C12.9619 14.966 13.4172 14.7775 13.8104 14.4918C14.2036 14.2061 14.5236 13.8314 14.7443 13.3983C14.965 12.9652 15.08 12.4861 15.08 12L23 12Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
15
pkg/grid/src/favicon.svg
Normal file
15
pkg/grid/src/favicon.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
28
pkg/grid/src/logic/useTreaty.ts
Normal file
28
pkg/grid/src/logic/useTreaty.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import clipboardCopy from 'clipboard-copy';
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { installDocket, requestTreaty, treatyKey } from '../state/docket';
|
||||
|
||||
export function useTreaty() {
|
||||
const { ship, desk } = useParams<{ ship: string; desk: string }>();
|
||||
const { data: treaty } = useQuery(treatyKey([ship, desk]), () => requestTreaty(ship, desk));
|
||||
const { mutate, ...installStatus } = useMutation(() => installDocket(ship, desk));
|
||||
|
||||
const copyApp = useCallback(async () => {
|
||||
clipboardCopy(`${ship}/${desk}`);
|
||||
}, [ship, desk]);
|
||||
|
||||
const installApp = useCallback(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
ship,
|
||||
desk,
|
||||
treaty,
|
||||
installStatus,
|
||||
installApp,
|
||||
copyApp
|
||||
};
|
||||
}
|
37
pkg/grid/src/logic/useWaitForProps.ts
Normal file
37
pkg/grid/src/logic/useWaitForProps.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export function useWaitForProps<P>(props: P, timeout = 0) {
|
||||
const [mainResolve, setMainResolve] = useState<() => void>(() => () => {});
|
||||
const [ready, setReady] = useState<(p: P) => boolean | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof ready === 'function' && ready(props)) {
|
||||
mainResolve();
|
||||
}
|
||||
}, [props, ready, mainResolve]);
|
||||
|
||||
/**
|
||||
* Waits until some predicate is true
|
||||
*
|
||||
* @param r - Predicate to wait for
|
||||
* @returns A promise that resolves when `r` returns true, or rejects if the
|
||||
* waiting times out
|
||||
*
|
||||
*/
|
||||
const waiter = useCallback(
|
||||
(r: (props: P) => boolean) => {
|
||||
setReady(() => r);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
setMainResolve(() => resolve);
|
||||
if (timeout > 0) {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timed out'));
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
},
|
||||
[setMainResolve, setReady, timeout]
|
||||
);
|
||||
|
||||
return waiter;
|
||||
}
|
11
pkg/grid/src/main.tsx
Normal file
11
pkg/grid/src/main.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './app';
|
||||
import './styles/index.css';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('app')
|
||||
);
|
20
pkg/grid/src/nav/Help.tsx
Normal file
20
pkg/grid/src/nav/Help.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavStore } from './Nav';
|
||||
|
||||
export const Help = () => {
|
||||
const select = useNavStore((state) => state.select);
|
||||
|
||||
useEffect(() => {
|
||||
select('Help and Support');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-8 space-y-8">
|
||||
<h2 className="h4 text-gray-500">Recent Apps</h2>
|
||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
||||
<hr className="-mx-4 md:-mx-8" />
|
||||
<h2 className="h4 text-gray-500">Recent Developers</h2>
|
||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
||||
</div>
|
||||
);
|
||||
};
|
256
pkg/grid/src/nav/Nav.tsx
Normal file
256
pkg/grid/src/nav/Nav.tsx
Normal file
@ -0,0 +1,256 @@
|
||||
import { DialogContent } from '@radix-ui/react-dialog';
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
FocusEvent,
|
||||
FunctionComponent,
|
||||
KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import { Link, Route, Switch, useHistory, useLocation } from 'react-router-dom';
|
||||
import create from 'zustand';
|
||||
import { Dialog } from '../components/Dialog';
|
||||
import { Cross } from '../components/icons/Cross';
|
||||
import { Help } from './Help';
|
||||
import { Notifications } from './Notifications';
|
||||
import { Search } from './Search';
|
||||
import { SystemMenu } from './SystemMenu';
|
||||
import { SystemPreferences } from './SystemPreferences';
|
||||
|
||||
export type MenuState =
|
||||
| 'closed'
|
||||
| 'search'
|
||||
| 'notifications'
|
||||
| 'help-and-support'
|
||||
| 'system-preferences';
|
||||
|
||||
interface NavProps {
|
||||
menu?: MenuState;
|
||||
}
|
||||
|
||||
interface NavStore {
|
||||
searchInput: string;
|
||||
setSearchInput: (input: string) => void;
|
||||
selection: React.ReactNode;
|
||||
select: (selection: React.ReactNode, input?: string) => void;
|
||||
}
|
||||
|
||||
export const useNavStore = create<NavStore>((set) => ({
|
||||
searchInput: '',
|
||||
setSearchInput: (input: string) => set({ searchInput: input }),
|
||||
selection: null,
|
||||
select: (selection: React.ReactNode, input?: string) =>
|
||||
set({ searchInput: input || '', selection })
|
||||
}));
|
||||
|
||||
export function createNextPath(current: string, nextPart?: string): string {
|
||||
let end = nextPart;
|
||||
const parts = current.split('/').reverse();
|
||||
if (parts[1] === 'search') {
|
||||
end = 'apps';
|
||||
}
|
||||
|
||||
if (parts[0] === 'leap') {
|
||||
end = `search/${nextPart}`;
|
||||
}
|
||||
|
||||
return `${current}/${end}`;
|
||||
}
|
||||
|
||||
export function createPreviousPath(current: string): string {
|
||||
const parts = current.split('/');
|
||||
parts.pop();
|
||||
|
||||
if (parts[parts.length - 1] === 'leap') {
|
||||
parts.push('search');
|
||||
}
|
||||
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
export const Nav: FunctionComponent<NavProps> = ({ menu = 'closed' }) => {
|
||||
const { push } = useHistory();
|
||||
const location = useLocation();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { searchInput, setSearchInput, selection, select } = useNavStore();
|
||||
const [systemMenuOpen, setSystemMenuOpen] = useState(false);
|
||||
|
||||
const isOpen = menu !== 'closed';
|
||||
const eitherOpen = isOpen || systemMenuOpen;
|
||||
|
||||
const onOpen = useCallback(
|
||||
(event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (menu === 'search' && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
},
|
||||
[menu]
|
||||
);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!menu || menu === 'search') {
|
||||
// select(null);
|
||||
// inputRef.current?.focus();
|
||||
// }
|
||||
// }, [menu]);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, [selection]);
|
||||
|
||||
const toggleSearch = useCallback(() => {
|
||||
if (selection || menu === 'search') {
|
||||
return;
|
||||
}
|
||||
|
||||
push('/leap/search');
|
||||
}, [selection, menu]);
|
||||
|
||||
const onDialogClose = useCallback((open: boolean) => {
|
||||
if (!open) {
|
||||
select(null);
|
||||
push('/');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onDialogKey = useCallback(
|
||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!selection || searchInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||
e.preventDefault();
|
||||
select(null);
|
||||
const pathBack = createPreviousPath(location.pathname);
|
||||
push(pathBack);
|
||||
}
|
||||
},
|
||||
[selection, searchInput, location.pathname]
|
||||
);
|
||||
|
||||
const onFocus = useCallback((e: FocusEvent<HTMLInputElement>) => {
|
||||
// refocusing tab with input focused is false trigger
|
||||
const windowFocus = e.nativeEvent.currentTarget === document.body;
|
||||
if (windowFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleSearch();
|
||||
}, []);
|
||||
|
||||
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const value = input.value.trim();
|
||||
setSearchInput(value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<menu className="w-full max-w-3xl my-6 px-4 text-gray-400 font-semibold">
|
||||
<div className={classNames('flex space-x-2', isOpen && 'invisible')}>
|
||||
{!isOpen && (
|
||||
<SystemMenu
|
||||
showOverlay
|
||||
open={systemMenuOpen}
|
||||
setOpen={setSystemMenuOpen}
|
||||
className={classNames(
|
||||
'relative z-50 flex-none',
|
||||
eitherOpen ? 'bg-white' : 'bg-gray-100'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Link
|
||||
to="/leap/notifications"
|
||||
className="relative z-50 flex-none circle-button bg-blue-400 text-white default-ring"
|
||||
>
|
||||
3
|
||||
</Link>
|
||||
<input
|
||||
onClick={toggleSearch}
|
||||
onFocus={onFocus}
|
||||
type="text"
|
||||
className="relative z-50 rounded-full w-full pl-4 h4 bg-gray-100 default-ring"
|
||||
placeholder="Search Landscape"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={onDialogClose}>
|
||||
<DialogContent
|
||||
onOpenAutoFocus={onOpen}
|
||||
className="fixed top-0 left-[calc(50%-7.5px)] w-[calc(100%-15px)] max-w-3xl px-4 text-gray-400 -translate-x-1/2 outline-none"
|
||||
>
|
||||
<div tabIndex={-1} onKeyDown={onDialogKey} role="presentation">
|
||||
<header className="flex my-6 space-x-2">
|
||||
<SystemMenu
|
||||
open={systemMenuOpen}
|
||||
setOpen={setSystemMenuOpen}
|
||||
className={classNames(
|
||||
'relative z-50 flex-none',
|
||||
eitherOpen ? 'bg-white' : 'bg-gray-100'
|
||||
)}
|
||||
/>
|
||||
<Link
|
||||
to="/leap/notifications"
|
||||
className="relative z-50 flex-none circle-button bg-blue-400 text-white"
|
||||
>
|
||||
3
|
||||
</Link>
|
||||
<div className="relative z-50 flex items-center w-full px-2 rounded-full bg-white default-ring focus-within:ring-4">
|
||||
<label
|
||||
htmlFor="leap"
|
||||
className={classNames(
|
||||
'inline-block flex-none p-2 h4 text-blue-400',
|
||||
!selection && 'sr-only'
|
||||
)}
|
||||
>
|
||||
{selection || 'Search Landscape'}
|
||||
</label>
|
||||
<input
|
||||
id="leap"
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
placeholder={selection ? '' : 'Search Landscape'}
|
||||
className="flex-1 w-full h-full px-2 h4 rounded-full bg-transparent outline-none"
|
||||
value={searchInput}
|
||||
onClick={toggleSearch}
|
||||
onFocus={onFocus}
|
||||
onChange={onChange}
|
||||
role="combobox"
|
||||
aria-controls="leap-items"
|
||||
aria-expanded
|
||||
/>
|
||||
{(selection || searchInput) && (
|
||||
<Link
|
||||
to="/"
|
||||
className="circle-button w-8 h-8 text-gray-400 bg-gray-100 default-ring"
|
||||
onClick={() => select(null)}
|
||||
>
|
||||
<Cross className="w-3 h-3 fill-current" />
|
||||
<span className="sr-only">Close</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
id="leap-items"
|
||||
className="grid grid-rows-[fit-content(calc(100vh-7.5rem))] bg-white rounded-3xl overflow-hidden"
|
||||
role="listbox"
|
||||
>
|
||||
<Switch>
|
||||
<Route path="/leap/notifications" component={Notifications} />
|
||||
<Route path="/leap/system-preferences" component={SystemPreferences} />
|
||||
<Route path="/leap/help-and-support" component={Help} />
|
||||
<Route path={['/leap/search', '/leap']} component={Search} />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</menu>
|
||||
);
|
||||
};
|
20
pkg/grid/src/nav/Notifications.tsx
Normal file
20
pkg/grid/src/nav/Notifications.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavStore } from './Nav';
|
||||
|
||||
export const Notifications = () => {
|
||||
const select = useNavStore((state) => state.select);
|
||||
|
||||
useEffect(() => {
|
||||
select('Notifications');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-8 space-y-8">
|
||||
<h2 className="h4 text-gray-500">Recent Apps</h2>
|
||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
||||
<hr className="-mx-4 md:-mx-8" />
|
||||
<h2 className="h4 text-gray-500">Recent Developers</h2>
|
||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
||||
</div>
|
||||
);
|
||||
};
|
21
pkg/grid/src/nav/Search.tsx
Normal file
21
pkg/grid/src/nav/Search.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import { AppInfo } from './search/AppInfo';
|
||||
import { Apps } from './search/Apps';
|
||||
import { Home } from './search/Home';
|
||||
import { Providers } from './search/Providers';
|
||||
|
||||
type SearchProps = RouteComponentProps<{
|
||||
query?: string;
|
||||
}>;
|
||||
|
||||
export const Search = ({ match }: SearchProps) => {
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${match.path}/:ship/apps/:desk`} component={AppInfo} />
|
||||
<Route path={`${match.path}/:ship/apps`} component={Apps} />
|
||||
<Route path={`${match.path}/:ship`} component={Providers} />
|
||||
<Route path={`${match.path}`} component={Home} />
|
||||
</Switch>
|
||||
);
|
||||
};
|
89
pkg/grid/src/nav/SystemMenu.tsx
Normal file
89
pkg/grid/src/nav/SystemMenu.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import classNames from 'classnames';
|
||||
import clipboardCopy from 'clipboard-copy';
|
||||
import React, { HTMLAttributes, useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Adjust } from '../components/icons/Adjust';
|
||||
|
||||
type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
showOverlay?: boolean;
|
||||
};
|
||||
|
||||
export const SystemMenu = ({ open, setOpen, className, showOverlay = false }: SystemMenuProps) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyHash = useCallback((event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
setCopied(true);
|
||||
clipboardCopy('fjuhl');
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1250);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.Root open={open} onOpenChange={(isOpen) => setOpen(isOpen)}>
|
||||
<DropdownMenu.Trigger
|
||||
className={classNames('circle-button default-ring', open && 'text-gray-300', className)}
|
||||
>
|
||||
<Adjust className="w-6 h-6 fill-current" />
|
||||
<span className="sr-only">System Menu</span>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
sideOffset={12}
|
||||
className="dropdown min-w-64 p-6 font-semibold text-gray-500 bg-white"
|
||||
>
|
||||
<DropdownMenu.Group className="space-y-6">
|
||||
<DropdownMenu.Item
|
||||
as={Link}
|
||||
to="/leap/system-preferences"
|
||||
className="flex items-center space-x-2 default-ring ring-offset-2 rounded"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setTimeout(() => setOpen(false), 0);
|
||||
}}
|
||||
>
|
||||
<span className="w-5 h-5 bg-gray-100 rounded-full" />
|
||||
<span className="h4">System Preferences</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
as={Link}
|
||||
to="/leap/help-and-support"
|
||||
className="flex items-center space-x-2 default-ring ring-offset-2 rounded"
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setTimeout(() => setOpen(false), 0);
|
||||
}}
|
||||
>
|
||||
<span className="w-5 h-5 bg-gray-100 rounded-full" />
|
||||
<span className="h4">Help and Support</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
as="button"
|
||||
className="inline-flex items-center py-2 px-3 h4 text-black bg-gray-100 rounded default-ring"
|
||||
onSelect={copyHash}
|
||||
>
|
||||
<span className="sr-only">Base Hash</span>
|
||||
<code>
|
||||
{!copied && <span aria-label="f-j-u-h-l">fjuhl</span>}
|
||||
{copied && 'copied!'}
|
||||
</code>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
{showOverlay && open && (
|
||||
<div className="fixed z-30 right-0 bottom-0 w-screen h-screen bg-black opacity-30" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
20
pkg/grid/src/nav/SystemPreferences.tsx
Normal file
20
pkg/grid/src/nav/SystemPreferences.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavStore } from './Nav';
|
||||
|
||||
export const SystemPreferences = () => {
|
||||
const select = useNavStore((state) => state.select);
|
||||
|
||||
useEffect(() => {
|
||||
select('System Preferences');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-8 space-y-8">
|
||||
<h2 className="h4 text-gray-500">Recent Apps</h2>
|
||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
||||
<hr className="-mx-4 md:-mx-8" />
|
||||
<h2 className="h4 text-gray-500">Recent Developers</h2>
|
||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
||||
</div>
|
||||
);
|
||||
};
|
64
pkg/grid/src/nav/search/AppInfo.tsx
Normal file
64
pkg/grid/src/nav/search/AppInfo.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { PillButton } from '../../components/Button';
|
||||
import { DocketHeader } from '../../components/DocketHeader';
|
||||
import { ShipName } from '../../components/ShipName';
|
||||
import { Spinner } from '../../components/Spinner';
|
||||
import { TreatyMeta } from '../../components/TreatyMeta';
|
||||
import { useTreaty } from '../../logic/useTreaty';
|
||||
import { chargesKey, fetchCharges } from '../../state/docket';
|
||||
import { useNavStore } from '../Nav';
|
||||
|
||||
export const AppInfo = () => {
|
||||
const select = useNavStore((state) => state.select);
|
||||
const { ship, desk, treaty, installStatus, copyApp, installApp } = useTreaty();
|
||||
const { data: charges } = useQuery(chargesKey(), fetchCharges);
|
||||
const installed = (charges || {})[desk] || installStatus.isSuccess;
|
||||
|
||||
useEffect(() => {
|
||||
select(
|
||||
<>
|
||||
Apps by <ShipName name={ship} className="font-mono" />: {treaty?.title}
|
||||
</>
|
||||
);
|
||||
}, [treaty?.title]);
|
||||
|
||||
if (!treaty) {
|
||||
// TODO: maybe replace spinner with skeletons
|
||||
return (
|
||||
<div className="dialog-inner-container text-black">
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dialog-inner-container text-black">
|
||||
<DocketHeader docket={treaty}>
|
||||
<div className="col-span-2 md:col-span-1 flex items-center space-x-4">
|
||||
{installed && (
|
||||
<PillButton as="a" href={`/apps/${treaty.base}`} target={treaty.title || '_blank'}>
|
||||
Open App
|
||||
</PillButton>
|
||||
)}
|
||||
{!installed && (
|
||||
<PillButton onClick={installApp}>
|
||||
{installStatus.isIdle && 'Get App'}
|
||||
{installStatus.isLoading && (
|
||||
<>
|
||||
<Spinner />
|
||||
<span className="sr-only">Installing...</span>
|
||||
</>
|
||||
)}
|
||||
</PillButton>
|
||||
)}
|
||||
<PillButton variant="secondary" onClick={copyApp}>
|
||||
Copy App Link
|
||||
</PillButton>
|
||||
</div>
|
||||
</DocketHeader>
|
||||
<hr className="-mx-5 sm:-mx-8" />
|
||||
<TreatyMeta treaty={treaty} />
|
||||
</div>
|
||||
);
|
||||
};
|
79
pkg/grid/src/nav/search/Apps.tsx
Normal file
79
pkg/grid/src/nav/search/Apps.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useQuery, useQueryClient } from 'react-query';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import slugify from 'slugify';
|
||||
import { ShipName } from '../../components/ShipName';
|
||||
import { fetchProviderTreaties, treatyKey } from '../../state/docket';
|
||||
import { Treaty } from '../../state/docket-types';
|
||||
import { useNavStore } from '../Nav';
|
||||
|
||||
type AppsProps = RouteComponentProps<{ ship: string }>;
|
||||
|
||||
export const Apps = ({ match }: AppsProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { select } = useNavStore();
|
||||
const provider = match?.params.ship;
|
||||
const { data } = useQuery(treatyKey([provider]), () => fetchProviderTreaties(provider), {
|
||||
enabled: !!provider
|
||||
});
|
||||
const count = data?.length;
|
||||
|
||||
useEffect(() => {
|
||||
select(
|
||||
<>
|
||||
Apps by <ShipName name={provider} className="font-mono" />
|
||||
</>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const preloadApp = useCallback(
|
||||
(app: Treaty) => {
|
||||
queryClient.setQueryData(treatyKey([provider, app.desk]), app);
|
||||
},
|
||||
[queryClient]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400">
|
||||
<div id="developed-by">
|
||||
<h2 className="mb-3">
|
||||
Software developed by <ShipName name={provider} className="font-mono" />
|
||||
</h2>
|
||||
<p>
|
||||
{count} result{count === 1 ? '' : 's'}
|
||||
</p>
|
||||
</div>
|
||||
{data && (
|
||||
<ul className="space-y-8" aria-labelledby="developed-by">
|
||||
{data.map((app) => (
|
||||
<li key={app.desk}>
|
||||
<Link
|
||||
to={`${match?.path.replace(':ship', provider)}/${slugify(app.desk)}`}
|
||||
className="flex items-center space-x-3 default-ring ring-offset-2 rounded-lg"
|
||||
onClick={() => preloadApp(app)}
|
||||
>
|
||||
<div
|
||||
className="flex-none relative w-12 h-12 bg-gray-200 rounded-lg"
|
||||
style={{ backgroundColor: app.color }}
|
||||
>
|
||||
{app.img && (
|
||||
<img
|
||||
className="absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2"
|
||||
src={app.img}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 text-black">
|
||||
<p>{app.title}</p>
|
||||
{app.info && <p className="font-normal">{app.info}</p>}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<p>That's it!</p>
|
||||
</div>
|
||||
);
|
||||
};
|
35
pkg/grid/src/nav/search/Home.tsx
Normal file
35
pkg/grid/src/nav/search/Home.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { debounce } from 'lodash-es';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { createNextPath, useNavStore } from '../Nav';
|
||||
|
||||
type HomeProps = RouteComponentProps;
|
||||
|
||||
export const Home = ({ match, history }: HomeProps) => {
|
||||
const searchInput = useNavStore((state) => state.searchInput);
|
||||
const { push } = history;
|
||||
const { path } = match;
|
||||
|
||||
const handleSearch = useCallback(
|
||||
debounce((input: string) => {
|
||||
push(createNextPath(path, input.trim()));
|
||||
}, 300),
|
||||
[path]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchInput) {
|
||||
handleSearch(searchInput);
|
||||
}
|
||||
}, [searchInput]);
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 md:p-8 space-y-8 overflow-y-auto">
|
||||
<h2 className="h4 text-gray-500">Recent Apps</h2>
|
||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
||||
<hr className="-mx-4 md:-mx-8" />
|
||||
<h2 className="h4 text-gray-500">Recent Developers</h2>
|
||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
||||
</div>
|
||||
);
|
||||
};
|
73
pkg/grid/src/nav/search/Providers.tsx
Normal file
73
pkg/grid/src/nav/search/Providers.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { debounce } from 'lodash-es';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import { ShipName } from '../../components/ShipName';
|
||||
import { fetchProviders, providersKey } from '../../state/docket';
|
||||
import { useNavStore } from '../Nav';
|
||||
|
||||
type ProvidersProps = RouteComponentProps<{ ship: string }>;
|
||||
|
||||
export const Providers = ({ match, history }: ProvidersProps) => {
|
||||
const { searchInput, select } = useNavStore((state) => ({
|
||||
searchInput: state.searchInput,
|
||||
select: state.select
|
||||
}));
|
||||
const { push } = history;
|
||||
const { path } = match;
|
||||
const provider = match?.params.ship;
|
||||
const { data } = useQuery(providersKey([provider]), () => fetchProviders(provider), {
|
||||
enabled: !!provider,
|
||||
keepPreviousData: true
|
||||
});
|
||||
const count = data?.length;
|
||||
|
||||
useEffect(() => {
|
||||
select(null, provider);
|
||||
}, []);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
debounce((input: string) => {
|
||||
push(match?.path.replace(':ship', input.trim()));
|
||||
}, 300),
|
||||
[path]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchInput) {
|
||||
handleSearch(searchInput);
|
||||
}
|
||||
}, [searchInput]);
|
||||
|
||||
return (
|
||||
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400" aria-live="polite">
|
||||
<div id="providers">
|
||||
<h2 className="mb-3">Searching Software Providers</h2>
|
||||
<p>
|
||||
{count} result{count === 1 ? '' : 's'}
|
||||
</p>
|
||||
</div>
|
||||
{data && (
|
||||
<ul className="space-y-8" aria-labelledby="providers">
|
||||
{data.map((p) => (
|
||||
<li key={p.shipName}>
|
||||
<Link
|
||||
to={`${match?.path.replace(':ship', p.shipName)}/apps`}
|
||||
className="flex items-center space-x-3 default-ring ring-offset-2 rounded-lg"
|
||||
>
|
||||
<div className="flex-none relative w-12 h-12 bg-black rounded-lg">
|
||||
{/* TODO: Handle sigils */}
|
||||
</div>
|
||||
<div className="flex-1 text-black">
|
||||
<p className="font-mono">{p.nickname || <ShipName name={p.shipName} />}</p>
|
||||
{p.status && <p className="font-normal">{p.status}</p>}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<p>That's it!</p>
|
||||
</div>
|
||||
);
|
||||
};
|
53
pkg/grid/src/pages/Grid.tsx
Normal file
53
pkg/grid/src/pages/Grid.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { map } from 'lodash-es';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { useQuery, useQueryClient } from 'react-query';
|
||||
import { Route, RouteComponentProps } from 'react-router-dom';
|
||||
import { MenuState, Nav } from '../nav/Nav';
|
||||
import { chargesKey, fetchCharges } from '../state/docket';
|
||||
import { Treaties } from '../state/docket-types';
|
||||
import { RemoveApp } from '../tiles/RemoveApp';
|
||||
import { SuspendApp } from '../tiles/SuspendApp';
|
||||
import { Tile } from '../tiles/Tile';
|
||||
|
||||
type GridProps = RouteComponentProps<{
|
||||
menu?: MenuState;
|
||||
}>;
|
||||
|
||||
export const Grid: FunctionComponent<GridProps> = ({ match }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
data: charges,
|
||||
isLoading,
|
||||
isSuccess
|
||||
} = useQuery(chargesKey(), fetchCharges, {
|
||||
onSuccess: (dockets: Treaties) => {
|
||||
Object.entries(dockets).forEach(([k, v]) => {
|
||||
queryClient.setQueryData(chargesKey([k]), v);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<header className="sticky top-0 left-0 z-30 flex justify-center w-full bg-white">
|
||||
<Nav menu={match.params.menu} />
|
||||
</header>
|
||||
|
||||
<main className="h-full w-full flex justify-center pt-24 pb-32 relative z-0">
|
||||
{isLoading && <span>Loading...</span>}
|
||||
{isSuccess && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6 px-4 md:px-8 w-full max-w-6xl">
|
||||
{charges &&
|
||||
map(charges, (charge, desk) => <Tile key={desk} docket={charge} desk={desk} />)}
|
||||
</div>
|
||||
)}
|
||||
<Route exact path="/app/:desk/suspend">
|
||||
<SuspendApp />
|
||||
</Route>
|
||||
<Route exact path="/app/:desk/remove">
|
||||
<RemoveApp />
|
||||
</Route>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
20
pkg/grid/src/state/api.ts
Normal file
20
pkg/grid/src/state/api.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import Urbit from '@urbit/http-api';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ship: string;
|
||||
}
|
||||
}
|
||||
|
||||
const api =
|
||||
import.meta.env.MODE === 'mock'
|
||||
? {
|
||||
poke: () => {},
|
||||
subscribe: () => {},
|
||||
subscribeOnce: () => {},
|
||||
ship: ''
|
||||
}
|
||||
: new Urbit('', '');
|
||||
|
||||
api.ship = window.ship;
|
||||
export default api;
|
36
pkg/grid/src/state/docket-types.ts
Normal file
36
pkg/grid/src/state/docket-types.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export type DeskStatus = 'active' | 'suspended';
|
||||
|
||||
export interface Docket {
|
||||
title: string;
|
||||
info?: string;
|
||||
color: string;
|
||||
base: string;
|
||||
website: string;
|
||||
license: string;
|
||||
version: string;
|
||||
|
||||
// yet to be implemented
|
||||
img?: string;
|
||||
status: DeskStatus;
|
||||
}
|
||||
|
||||
export interface Treaty extends Docket {
|
||||
ship: string;
|
||||
desk: string;
|
||||
cass: string;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export interface Dockets {
|
||||
[ref: string]: Docket;
|
||||
}
|
||||
|
||||
export interface Treaties {
|
||||
[ref: string]: Treaty;
|
||||
}
|
||||
|
||||
export interface Provider {
|
||||
shipName: string;
|
||||
nickname?: string;
|
||||
status?: string;
|
||||
}
|
181
pkg/grid/src/state/docket.ts
Normal file
181
pkg/grid/src/state/docket.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import fuzzy from 'fuzzy';
|
||||
import { omit } from 'lodash-es';
|
||||
import { queryClient } from '../app';
|
||||
import api from './api';
|
||||
import { Treaty, Dockets, Docket, Provider } from './docket-types';
|
||||
import { providers, treaties } from './mock-data';
|
||||
|
||||
const useMockData = import.meta.env.MODE === 'mock';
|
||||
|
||||
function makeKeyFn(key: string) {
|
||||
return (childKeys: string[] = []) => {
|
||||
return [key].concat(childKeys);
|
||||
};
|
||||
}
|
||||
|
||||
export const chargesKey = makeKeyFn('charges');
|
||||
|
||||
export const providersKey = makeKeyFn('providers');
|
||||
|
||||
export const treatyKey = makeKeyFn('treaty');
|
||||
|
||||
async function fakeRequest<T>(data: T, time = 300): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(data);
|
||||
}, time);
|
||||
});
|
||||
}
|
||||
|
||||
const stableTreatyMap = new Map<string, Treaty[]>();
|
||||
|
||||
interface ChargesResponse {
|
||||
initial: Dockets;
|
||||
}
|
||||
|
||||
export async function fetchCharges(): Promise<Dockets> {
|
||||
const charges = queryClient.getQueryData<Dockets>(chargesKey());
|
||||
if (useMockData && charges) {
|
||||
return charges;
|
||||
}
|
||||
|
||||
const dockets = useMockData
|
||||
? await fakeRequest(treaties)
|
||||
: ((await (await fetch('/~/scry/docket/charges.json')).json()) as ChargesResponse).initial;
|
||||
|
||||
return Object.entries(dockets).reduce((obj: Dockets, [key, value]) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj[key] = normalizeDocket(value);
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export async function fetchProviders(query?: string): Promise<Provider[]> {
|
||||
const searchTexts = providers.map((p) => p.shipName + (p.nickname || ''));
|
||||
return fakeRequest(fuzzy.filter(query || '', searchTexts).map((el) => providers[el.index]));
|
||||
}
|
||||
|
||||
export async function fetchProviderTreaties(provider: string): Promise<Treaty[]> {
|
||||
const treatyList = Object.values(treaties).map(normalizeDocket);
|
||||
|
||||
if (!stableTreatyMap.has(provider)) {
|
||||
stableTreatyMap.set(
|
||||
provider,
|
||||
treatyList.filter(() => !!Math.round(Math.random()))
|
||||
);
|
||||
}
|
||||
|
||||
return fakeRequest(stableTreatyMap.get(provider) || []);
|
||||
}
|
||||
|
||||
export async function requestTreaty(ship: string, desk: string): Promise<Treaty> {
|
||||
if (useMockData) {
|
||||
return fakeRequest(treaties[desk]);
|
||||
}
|
||||
|
||||
const key = `${ship}/${desk}`;
|
||||
const result = await api.subscribeOnce('docket', `/treaty/${key}`, 20000);
|
||||
return { ...normalizeDocket(result), ship, desk };
|
||||
}
|
||||
|
||||
export async function installDocket(ship: string, desk: string): Promise<number | void> {
|
||||
if (useMockData) {
|
||||
const docket = normalizeDocket(await requestTreaty(ship, desk));
|
||||
const charges = await queryClient.fetchQuery(chargesKey(), fetchCharges);
|
||||
addCharge(charges, { desk, docket });
|
||||
}
|
||||
|
||||
return api.poke({
|
||||
app: 'hood',
|
||||
mark: 'kiln-install',
|
||||
json: {
|
||||
ship,
|
||||
desk,
|
||||
local: desk
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function uninstallDocket(desk: string): Promise<number | void> {
|
||||
if (useMockData) {
|
||||
const charges = await queryClient.fetchQuery(chargesKey(), fetchCharges);
|
||||
delCharge(charges, desk);
|
||||
}
|
||||
|
||||
return api.poke({
|
||||
app: 'docket',
|
||||
mark: 'docket-uninstall',
|
||||
json: desk
|
||||
});
|
||||
}
|
||||
|
||||
export async function toggleDocket(desk: string): Promise<void> {
|
||||
const charges = await queryClient.fetchQuery(chargesKey(), fetchCharges);
|
||||
const docket = (charges || {})[desk];
|
||||
docket.status = docket.status === 'active' ? 'suspended' : 'active';
|
||||
}
|
||||
|
||||
function normalizeDocket<T extends Docket>(docket: T): T {
|
||||
return {
|
||||
...docket,
|
||||
status: docket.status || 'active',
|
||||
color: `#${docket.color.slice(2).replace('.', '')}`.toUpperCase()
|
||||
};
|
||||
}
|
||||
|
||||
interface AddDockEvent {
|
||||
'add-dock': {
|
||||
desk: string;
|
||||
docket: Docket;
|
||||
};
|
||||
}
|
||||
|
||||
interface DelDockEvent {
|
||||
'del-dock': string;
|
||||
}
|
||||
|
||||
type DocketEvent = AddDockEvent | DelDockEvent;
|
||||
|
||||
function addCharge(charges: Dockets | undefined, { desk, docket }: AddDockEvent['add-dock']) {
|
||||
queryClient.setQueryData(chargesKey(), { ...charges, [desk]: docket });
|
||||
}
|
||||
|
||||
function delCharge(charges: Dockets | undefined, desk: DelDockEvent['del-dock']) {
|
||||
queryClient.setQueryData(chargesKey(), omit(charges, desk));
|
||||
}
|
||||
|
||||
api.subscribe({
|
||||
app: 'docket',
|
||||
path: '/charges',
|
||||
event: (data: DocketEvent): void => {
|
||||
const charges = queryClient.getQueryData<Dockets>(chargesKey());
|
||||
console.log(data);
|
||||
|
||||
if ('add-dock' in data) {
|
||||
addCharge(charges, data['add-dock']);
|
||||
}
|
||||
|
||||
if ('del-dock' in data) {
|
||||
delCharge(charges, data['del-dock']);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// const selCharges = (s: DocketState) => {
|
||||
// return omit(s.charges, "grid");
|
||||
// };
|
||||
|
||||
// export function useCharges() {
|
||||
// return useDocketState(selCharges);
|
||||
// }
|
||||
|
||||
// export function useCharge(desk: string) {
|
||||
// return useDocketState(useCallback(state => state.charges[desk], [desk]));
|
||||
// }
|
||||
|
||||
// const selRequest = (s: DocketState) => s.request;
|
||||
// export function useRequestDocket() {
|
||||
// return useDocketState(selRequest);
|
||||
// }
|
||||
|
||||
// export default useDocketState;
|
168
pkg/grid/src/state/mock-data.ts
Normal file
168
pkg/grid/src/state/mock-data.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import systemUrl from '../assets/system.png';
|
||||
import goUrl from '../assets/go.png';
|
||||
import { Provider, Treaties, Treaty } from './docket-types';
|
||||
|
||||
export const providers: Provider[] = [
|
||||
{
|
||||
shipName: '~zod',
|
||||
nickname: 'Tlon Corporation'
|
||||
},
|
||||
{
|
||||
shipName: '~nocsyx-lassul',
|
||||
status: 'technomancing an electron wrapper for urbit'
|
||||
},
|
||||
{
|
||||
shipName: '~nachus-hollyn'
|
||||
},
|
||||
{
|
||||
shipName: '~nalbel_litzod',
|
||||
status: 'congratulations'
|
||||
},
|
||||
{
|
||||
shipName: '~litmus^ritten'
|
||||
},
|
||||
{
|
||||
shipName: '~nalput_litzod',
|
||||
status: 'Queen'
|
||||
},
|
||||
{
|
||||
shipName: '~nalrex_bannus',
|
||||
status: 'Script, command and inspect your Urbit. Use TUI applications'
|
||||
},
|
||||
{
|
||||
shipName: '~nalrys',
|
||||
status: 'hosting coming soon'
|
||||
}
|
||||
];
|
||||
|
||||
export const appMetaData: Pick<Treaty, 'cass' | 'hash' | 'website' | 'license' | 'version'> = {
|
||||
cass: '~2021.8.11..05.11.10..b721',
|
||||
hash: '0v6.nj6ls.l7unh.l9bhk.d839n.n8nlq.m2dmc.fj80i.pvqun.uhg6g.1kk0h',
|
||||
website: 'https://tlon.io',
|
||||
license: 'MIT',
|
||||
version: '2.0.1'
|
||||
};
|
||||
|
||||
export const treaties: Treaties = {
|
||||
groups: {
|
||||
ship: '~zod',
|
||||
desk: 'groups',
|
||||
title: 'Groups',
|
||||
info: 'Simple Software for Community Assembly',
|
||||
status: 'active',
|
||||
base: 'groups',
|
||||
color: '##CDE7EF',
|
||||
...appMetaData
|
||||
},
|
||||
messages: {
|
||||
title: 'Messages',
|
||||
ship: '~zod',
|
||||
desk: 'messages',
|
||||
status: 'active',
|
||||
base: 'messages',
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '##8BE789',
|
||||
...appMetaData
|
||||
},
|
||||
calls: {
|
||||
title: 'Calls',
|
||||
ship: '~zod',
|
||||
desk: 'calls',
|
||||
status: 'active',
|
||||
base: 'calls',
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '##C2D6BE',
|
||||
...appMetaData
|
||||
},
|
||||
'bitcoin-wallet': {
|
||||
title: 'Bitcoin Wallet',
|
||||
ship: '~zod',
|
||||
desk: 'bitcoin-wallet',
|
||||
status: 'active',
|
||||
base: 'bitcoin-wallet',
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '##F0AE70',
|
||||
...appMetaData
|
||||
},
|
||||
system: {
|
||||
title: 'System',
|
||||
ship: '~zod',
|
||||
desk: 'system',
|
||||
status: 'active',
|
||||
base: 'system',
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '##2D0118',
|
||||
img: systemUrl,
|
||||
...appMetaData
|
||||
},
|
||||
'my-apps': {
|
||||
title: 'My Apps',
|
||||
ship: '~zod',
|
||||
desk: 'groups',
|
||||
status: 'active',
|
||||
base: 'my-apps',
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '##D8B14E',
|
||||
...appMetaData
|
||||
},
|
||||
go: {
|
||||
title: 'Go',
|
||||
ship: '~zod',
|
||||
desk: 'go',
|
||||
status: 'active',
|
||||
base: 'go',
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '##A58E52',
|
||||
img: goUrl,
|
||||
...appMetaData
|
||||
},
|
||||
terminal: {
|
||||
title: 'Terminal',
|
||||
ship: '~zod',
|
||||
desk: 'terminal',
|
||||
status: 'active',
|
||||
base: 'terminal',
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '##2D382B',
|
||||
...appMetaData
|
||||
},
|
||||
pomodoro: {
|
||||
title: 'Pomodoro',
|
||||
ship: '~zod',
|
||||
desk: 'pomodoro',
|
||||
status: 'active',
|
||||
base: 'pomodoro',
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '##EE5432',
|
||||
...appMetaData
|
||||
},
|
||||
clocks: {
|
||||
title: 'Clocks',
|
||||
ship: '~zod',
|
||||
desk: 'clocks',
|
||||
status: 'active',
|
||||
base: 'clocks',
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '##DCDCDC',
|
||||
...appMetaData
|
||||
},
|
||||
uniswap: {
|
||||
title: 'Uniswap',
|
||||
ship: '~zod',
|
||||
desk: 'uniswap',
|
||||
status: 'active',
|
||||
base: 'uniswap',
|
||||
info: 'A lengthier description of the app down here',
|
||||
color: '##FDA1FF',
|
||||
...appMetaData
|
||||
},
|
||||
inbox: {
|
||||
title: 'Inbox',
|
||||
ship: '~zod',
|
||||
desk: 'inbox',
|
||||
status: 'active',
|
||||
base: 'inbox',
|
||||
color: '##FEFFBA',
|
||||
...appMetaData
|
||||
}
|
||||
};
|
19
pkg/grid/src/styles/base.css
Normal file
19
pkg/grid/src/styles/base.css
Normal file
@ -0,0 +1,19 @@
|
||||
.h1 {
|
||||
@apply text-2xl sm:text-3xl font-bold leading-relaxed tracking-tighter;
|
||||
}
|
||||
|
||||
.h2 {
|
||||
@apply text-xl sm:text-2xl font-semibold leading-snug tracking-tight;
|
||||
}
|
||||
|
||||
.h3 {
|
||||
@apply text-lg sm:text-xl font-semibold leading-relaxed tracking-tight;
|
||||
}
|
||||
|
||||
.h4 {
|
||||
@apply text-sm sm:text-base font-semibold leading-normal tracking-tight;
|
||||
}
|
||||
|
||||
.default-ring {
|
||||
@apply focus:ring-4 ring-blue-400 ring-opacity-80 focus:outline-none;
|
||||
}
|
27
pkg/grid/src/styles/components.css
Normal file
27
pkg/grid/src/styles/components.css
Normal file
@ -0,0 +1,27 @@
|
||||
.circle-button {
|
||||
@apply inline-flex items-center justify-center h-12 w-12 font-semibold rounded-full;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 font-semibold text-base tracking-tight rounded-lg cursor-pointer;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
@apply fixed z-40 top-1/2 left-1/2 min-w-80 transform -translate-x-1/2 -translate-y-1/2;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@apply relative p-5 sm:p-8 bg-white rounded-3xl;
|
||||
}
|
||||
|
||||
.dialog-inner-container {
|
||||
@apply h-full p-4 md:p-8 space-y-8 overflow-y-auto;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@apply min-w-52 p-4 space-y-4 rounded-xl;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
@apply inline-flex items-center w-6 h-6 animate-spin;
|
||||
}
|
8
pkg/grid/src/styles/index.css
Normal file
8
pkg/grid/src/styles/index.css
Normal file
@ -0,0 +1,8 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "./base.css";
|
||||
|
||||
@import "tailwindcss/components";
|
||||
@import "./components.css";
|
||||
|
||||
@import "tailwindcss/utilities";
|
||||
@import "./utilities.css";
|
0
pkg/grid/src/styles/utilities.css
Normal file
0
pkg/grid/src/styles/utilities.css
Normal file
39
pkg/grid/src/tiles/RemoveApp.tsx
Normal file
39
pkg/grid/src/tiles/RemoveApp.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { Button } from '../components/Button';
|
||||
import { Dialog, DialogContent } from '../components/Dialog';
|
||||
import { chargesKey, uninstallDocket } from '../state/docket';
|
||||
import { Docket } from '../state/docket-types';
|
||||
|
||||
export const RemoveApp = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const history = useHistory();
|
||||
const { desk } = useParams<{ desk: string }>();
|
||||
const { data: docket } = useQuery<Docket>(chargesKey([desk]));
|
||||
const { mutate } = useMutation(() => uninstallDocket(desk), {
|
||||
onSuccess: () => {
|
||||
history.push('/');
|
||||
queryClient.invalidateQueries(chargesKey());
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: add optimistic updates
|
||||
const handleRemoveApp = useCallback(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && history.push('/')}>
|
||||
<DialogContent>
|
||||
<h1 className="h4 mb-9">Remove “{docket?.title || ''}”</h1>
|
||||
<p className="text-base tracking-tight mb-4 pr-6">
|
||||
Explanatory writing about what data will be kept.
|
||||
</p>
|
||||
<Button variant="destructive" onClick={handleRemoveApp}>
|
||||
Remove
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
41
pkg/grid/src/tiles/SuspendApp.tsx
Normal file
41
pkg/grid/src/tiles/SuspendApp.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
import { Redirect, useHistory, useParams } from 'react-router-dom';
|
||||
import { Button } from '../components/Button';
|
||||
import { Dialog, DialogContent } from '../components/Dialog';
|
||||
import { chargesKey, toggleDocket } from '../state/docket';
|
||||
import { Docket } from '../state/docket-types';
|
||||
|
||||
export const SuspendApp = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const history = useHistory();
|
||||
const { desk } = useParams<{ desk: string }>();
|
||||
const { data: docket } = useQuery<Docket>(chargesKey([desk]));
|
||||
const { mutate } = useMutation(() => toggleDocket(desk), {
|
||||
onSuccess: () => {
|
||||
history.push('/');
|
||||
queryClient.invalidateQueries(chargesKey());
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: add optimistic updates
|
||||
const handleSuspendApp = useCallback(() => mutate(), []);
|
||||
|
||||
if (docket?.status === 'suspended') {
|
||||
<Redirect to="/" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && history.push('/')}>
|
||||
<DialogContent>
|
||||
<h1 className="h4 mb-9">Suspend “{docket?.title || ''}”</h1>
|
||||
<p className="text-base tracking-tight mb-4 pr-6">
|
||||
Suspending an app will freeze its current state, and render it unable
|
||||
</p>
|
||||
<Button variant="destructive" onClick={handleSuspendApp}>
|
||||
Suspend
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
69
pkg/grid/src/tiles/Tile.tsx
Normal file
69
pkg/grid/src/tiles/Tile.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { darken, hsla, lighten, parseToHsla, readableColorIsBlack } from 'color2k';
|
||||
import { TileMenu } from './TileMenu';
|
||||
import { Docket } from '../state/docket-types';
|
||||
|
||||
type TileProps = {
|
||||
docket: Docket;
|
||||
desk: string;
|
||||
};
|
||||
|
||||
function getMenuColor(color: string, lightText: boolean, active: boolean): string {
|
||||
const hslaColor = parseToHsla(color);
|
||||
const satAdjustedColor = hsla(
|
||||
hslaColor[0],
|
||||
active ? Math.max(0.2, hslaColor[1]) : 0,
|
||||
hslaColor[2],
|
||||
1
|
||||
);
|
||||
|
||||
return lightText ? lighten(satAdjustedColor, 0.1) : darken(satAdjustedColor, 0.1);
|
||||
}
|
||||
|
||||
export const Tile: FunctionComponent<TileProps> = ({ docket, desk }) => {
|
||||
const { title, base, color, img, status } = docket;
|
||||
const active = status === 'active';
|
||||
const lightText = !readableColorIsBlack(color);
|
||||
const menuColor = getMenuColor(color, lightText, active);
|
||||
const suspendColor = 'rgb(220,220,220)';
|
||||
|
||||
return (
|
||||
<a
|
||||
href={active ? `/apps/${base}/` : undefined}
|
||||
target={base}
|
||||
className={classNames(
|
||||
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-xl default-ring',
|
||||
!active && 'cursor-default'
|
||||
)}
|
||||
style={{ backgroundColor: active ? color || 'purple' : suspendColor }}
|
||||
>
|
||||
<div>
|
||||
<TileMenu
|
||||
desk={desk}
|
||||
active={active}
|
||||
menuColor={menuColor}
|
||||
lightText={lightText}
|
||||
className="absolute z-10 top-2.5 right-2.5 sm:top-4 sm:right-4 opacity-0 hover-none:opacity-100 focus:opacity-100 group-hover:opacity-100"
|
||||
/>
|
||||
<div className="h4 absolute bottom-4 left-4 lg:bottom-8 lg:left-8">
|
||||
<h3
|
||||
className={`${
|
||||
lightText && active ? 'text-gray-200' : 'text-gray-800'
|
||||
} mix-blend-hard-light`}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
{!active && <span className="text-gray-400">Suspended</span>}
|
||||
</div>
|
||||
{img && (
|
||||
<img
|
||||
className="absolute top-1/2 left-1/2 h-[40%] w-[40%] object-contain transform -translate-x-1/2 -translate-y-1/2"
|
||||
src={img}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
128
pkg/grid/src/tiles/TileMenu.tsx
Normal file
128
pkg/grid/src/tiles/TileMenu.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import React, { useState } from 'react';
|
||||
import type * as Polymorphic from '@radix-ui/react-polymorphic';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { chargesKey, toggleDocket } from '../state/docket';
|
||||
|
||||
export interface TileMenuProps {
|
||||
desk: string;
|
||||
lightText: boolean;
|
||||
menuColor: string;
|
||||
active: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MenuIcon = ({ className }: { className: string }) => (
|
||||
<svg className={classNames('fill-current', className)} viewBox="0 0 16 16">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M14 8.5H2V7.5H14V8.5Z" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M2 2.5H14V3.5H2V2.5Z" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M14 13.5H2V12.5H14V13.5Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
type ItemComponent = Polymorphic.ForwardRefComponent<
|
||||
Polymorphic.IntrinsicElement<typeof DropdownMenu.Item>,
|
||||
Polymorphic.OwnProps<typeof DropdownMenu.Item>
|
||||
>;
|
||||
|
||||
const Item = React.forwardRef(({ children, ...props }, ref) => (
|
||||
<DropdownMenu.Item
|
||||
ref={ref}
|
||||
{...props}
|
||||
className="block w-full px-4 py-1 leading-none rounded mix-blend-hard-light select-none default-ring ring-gray-600"
|
||||
>
|
||||
{children}
|
||||
</DropdownMenu.Item>
|
||||
)) as ItemComponent;
|
||||
|
||||
export const TileMenu = ({ desk, active, menuColor, lightText, className }: TileMenuProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { mutate } = useMutation(() => toggleDocket(desk), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(chargesKey());
|
||||
}
|
||||
});
|
||||
|
||||
const menuBg = { backgroundColor: menuColor };
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root open={open} onOpenChange={(isOpen) => setOpen(isOpen)}>
|
||||
<DropdownMenu.Trigger
|
||||
className={classNames(
|
||||
'flex items-center justify-center w-8 h-8 rounded-full transition-opacity duration-75 default-ring',
|
||||
open && 'opacity-100',
|
||||
className
|
||||
)}
|
||||
style={menuBg}
|
||||
// onMouseOver={() => queryClient.setQueryData(['apps', name], app)}
|
||||
>
|
||||
<MenuIcon
|
||||
className={classNames('w-4 h-4 mix-blend-hard-light', lightText && 'text-gray-100')}
|
||||
/>
|
||||
<span className="sr-only">Menu</span>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
align="start"
|
||||
alignOffset={-32}
|
||||
sideOffset={4}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
className={classNames(
|
||||
'dropdown font-semibold',
|
||||
lightText ? 'text-gray-100' : 'text-gray-800'
|
||||
)}
|
||||
style={menuBg}
|
||||
>
|
||||
<DropdownMenu.Group className="space-y-4">
|
||||
{/*
|
||||
TODO: revisit with Liam
|
||||
<Item as={Link} to={`/leap/search/${provider}/apps/${name.toLowerCase()}`} onSelect={(e) => { e.preventDefault(); setTimeout(() => setOpen(false), 0) }}>App Info</Item>
|
||||
*/}
|
||||
<Item
|
||||
as={Link}
|
||||
to={`/app/${desk}`}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setTimeout(() => setOpen(false), 0);
|
||||
}}
|
||||
>
|
||||
App Info
|
||||
</Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator className="-mx-4 my-2 border-t-2 border-solid border-gray-500 mix-blend-soft-light" />
|
||||
<DropdownMenu.Group className="space-y-4">
|
||||
{active && (
|
||||
<Item
|
||||
as={Link}
|
||||
to={`/app/${desk}/suspend`}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setTimeout(() => setOpen(false), 0);
|
||||
}}
|
||||
>
|
||||
Suspend App
|
||||
</Item>
|
||||
)}
|
||||
{!active && <Item onSelect={() => mutate()}>Resume App</Item>}
|
||||
<Item
|
||||
as={Link}
|
||||
to={`/app/${desk}/remove`}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setTimeout(() => setOpen(false), 0);
|
||||
}}
|
||||
>
|
||||
Remove App
|
||||
</Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Arrow
|
||||
className="w-4 h-[10px] fill-current -translate-x-10"
|
||||
style={{ color: menuColor }}
|
||||
/>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
};
|
1
pkg/grid/src/vite-env.d.ts
vendored
Normal file
1
pkg/grid/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
74
pkg/grid/tailwind.config.js
Normal file
74
pkg/grid/tailwind.config.js
Normal file
@ -0,0 +1,74 @@
|
||||
const colors = require('tailwindcss/colors');
|
||||
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
purge: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
transparent: "transparent",
|
||||
white: "#FFFFFF",
|
||||
black: "#000000",
|
||||
gray: {
|
||||
...colors.trueGray,
|
||||
100: "#F2F2F2",
|
||||
200: "#CCCCCC",
|
||||
300: "#B3B3B3",
|
||||
400: "#808080",
|
||||
500: "#666666",
|
||||
},
|
||||
blue: {
|
||||
100: "#E9F5FF",
|
||||
200: "#D3EBFF",
|
||||
300: "#BCE2FF",
|
||||
400: "#219DFF",
|
||||
},
|
||||
red: {
|
||||
100: "#FFF6F5",
|
||||
200: "#FFC6C3",
|
||||
400: "#FF4136",
|
||||
},
|
||||
green: {
|
||||
100: "#E6F5F0",
|
||||
200: "#B3E2D1",
|
||||
400: "#009F65",
|
||||
},
|
||||
yellow: {
|
||||
100: "#FFF9E6",
|
||||
200: "#FFEEB3",
|
||||
300: "#FFDD66",
|
||||
400: "#FFC700",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'"Inter"',
|
||||
'"Inter UI"',
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
'"San Francisco"',
|
||||
'"Helvetica Neue"',
|
||||
"Arial",
|
||||
"sans-serif",
|
||||
],
|
||||
mono: [
|
||||
'"Source Code Pro"',
|
||||
'"Roboto mono"',
|
||||
'"Courier New"',
|
||||
"monospace",
|
||||
],
|
||||
},
|
||||
minWidth: theme => theme('spacing'),
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {
|
||||
opacity: ['hover-none']
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
require('tailwindcss-touch')()
|
||||
],
|
||||
}
|
19
pkg/grid/tsconfig.json
Normal file
19
pkg/grid/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
33
pkg/grid/vite.config.ts
Normal file
33
pkg/grid/vite.config.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import analyze from 'rollup-plugin-analyzer';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import reactRefresh from '@vitejs/plugin-react-refresh';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => ({
|
||||
base: mode === 'mock' ? undefined : '/apps/grid/',
|
||||
server:
|
||||
mode === 'mock'
|
||||
? undefined
|
||||
: {
|
||||
proxy: {
|
||||
'^((?!/apps/grid).)*$': {
|
||||
target: 'http://localhost:8080'
|
||||
}
|
||||
}
|
||||
},
|
||||
build:
|
||||
mode !== 'profile'
|
||||
? undefined
|
||||
: {
|
||||
rollupOptions: {
|
||||
plugins: [
|
||||
analyze({
|
||||
limit: 20
|
||||
}),
|
||||
visualizer()
|
||||
]
|
||||
}
|
||||
},
|
||||
plugins: [reactRefresh()]
|
||||
}));
|
Loading…
Reference in New Issue
Block a user