mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-01 11:33:41 +03:00
Merge remote-tracking branch 'origin/hm/grid-system-update-flow' into lf/grid-kiln
This commit is contained in:
commit
aecaa3395d
@ -5,11 +5,14 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Landscape • Home</title>
|
<title>Landscape • Home</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body class="text-sm font-sans text-gray-900 bg-white antialiased">
|
<body class="text-sm leading-6 font-sans text-gray-900 bg-white antialiased">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
58
pkg/grid/src/components/AppLink.tsx
Normal file
58
pkg/grid/src/components/AppLink.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link, LinkProps } from 'react-router-dom';
|
||||||
|
import { Docket } from '@urbit/api';
|
||||||
|
import { getAppHref } from '../state/util';
|
||||||
|
|
||||||
|
type Sizes = 'xs' | 'small' | 'default';
|
||||||
|
|
||||||
|
export type AppLinkProps = Omit<LinkProps, 'to'> & {
|
||||||
|
app: Docket;
|
||||||
|
size?: Sizes;
|
||||||
|
selected?: boolean;
|
||||||
|
to?: (app: Docket) => LinkProps['to'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeMap: Record<Sizes, string> = {
|
||||||
|
xs: 'w-6 h-6 mr-2 rounded',
|
||||||
|
small: 'w-8 h-8 mr-3 rounded-lg',
|
||||||
|
default: 'w-12 h-12 mr-3 rounded-lg'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppLink = ({
|
||||||
|
app,
|
||||||
|
to,
|
||||||
|
size = 'default',
|
||||||
|
selected = false,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: AppLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={(to && to(app)) || getAppHref(app.href)}
|
||||||
|
className={classNames(
|
||||||
|
'flex items-center default-ring ring-offset-2 rounded-lg',
|
||||||
|
selected && 'ring-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames('flex-none relative bg-gray-200 rounded-lg', sizeMap[size])}
|
||||||
|
style={{ backgroundColor: app.color }}
|
||||||
|
>
|
||||||
|
{app.image && (
|
||||||
|
<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.image}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-black">
|
||||||
|
<p>{app.title}</p>
|
||||||
|
{app.info && size === 'default' && <p className="font-normal">{app.info}</p>}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
54
pkg/grid/src/components/AppList.tsx
Normal file
54
pkg/grid/src/components/AppList.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React, { MouseEvent, useCallback } from 'react';
|
||||||
|
import { Docket } from '@urbit/api';
|
||||||
|
import { MatchItem } from '../nav/Nav';
|
||||||
|
import { useRecentsStore } from '../nav/search/Home';
|
||||||
|
import { AppLink, AppLinkProps } from './AppLink';
|
||||||
|
|
||||||
|
type AppListProps = {
|
||||||
|
apps: Docket[];
|
||||||
|
labelledBy: string;
|
||||||
|
matchAgainst?: MatchItem;
|
||||||
|
onClick?: (e: MouseEvent<HTMLAnchorElement>, app: Docket) => void;
|
||||||
|
listClass?: string;
|
||||||
|
} & Omit<AppLinkProps, 'app' | 'onClick'>;
|
||||||
|
|
||||||
|
export function appMatches(target: Docket, match?: MatchItem): boolean {
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchValue = match.display || match.value;
|
||||||
|
return target.title === matchValue; // TODO: need desk name or something || target.href === matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppList = ({
|
||||||
|
apps,
|
||||||
|
labelledBy,
|
||||||
|
matchAgainst,
|
||||||
|
onClick,
|
||||||
|
listClass = 'space-y-8',
|
||||||
|
...props
|
||||||
|
}: AppListProps) => {
|
||||||
|
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
||||||
|
const selected = useCallback((app: Docket) => appMatches(app, matchAgainst), [matchAgainst]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={listClass} aria-labelledby={labelledBy}>
|
||||||
|
{apps.map((app) => (
|
||||||
|
<li key={app.title} id={app.title} role="option" aria-selected={selected(app)}>
|
||||||
|
<AppLink
|
||||||
|
{...props}
|
||||||
|
app={app}
|
||||||
|
selected={selected(app)}
|
||||||
|
onClick={(e) => {
|
||||||
|
addRecentApp(app);
|
||||||
|
if (onClick) {
|
||||||
|
onClick(e, app);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
@ -2,7 +2,13 @@ import React from 'react';
|
|||||||
import type * as Polymorphic from '@radix-ui/react-polymorphic';
|
import type * as Polymorphic from '@radix-ui/react-polymorphic';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
type ButtonVariant = 'primary' | 'secondary' | 'destructive';
|
type ButtonVariant =
|
||||||
|
| 'primary'
|
||||||
|
| 'secondary'
|
||||||
|
| 'caution'
|
||||||
|
| 'destructive'
|
||||||
|
| 'alt-primary'
|
||||||
|
| 'alt-secondary';
|
||||||
|
|
||||||
type PolymorphicButton = Polymorphic.ForwardRefComponent<
|
type PolymorphicButton = Polymorphic.ForwardRefComponent<
|
||||||
'button',
|
'button',
|
||||||
@ -12,9 +18,12 @@ type PolymorphicButton = Polymorphic.ForwardRefComponent<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
const variants: Record<ButtonVariant, string> = {
|
const variants: Record<ButtonVariant, string> = {
|
||||||
primary: 'text-white bg-blue-400',
|
primary: 'text-white bg-black',
|
||||||
secondary: 'text-blue-400 bg-blue-100',
|
secondary: 'text-black bg-gray-100',
|
||||||
destructive: 'text-white bg-red-400'
|
caution: 'text-white bg-orange-500',
|
||||||
|
destructive: 'text-white bg-red-400',
|
||||||
|
'alt-primary': 'text-white bg-blue-400',
|
||||||
|
'alt-secondary': 'text-blue-400 bg-blue-100'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Button = React.forwardRef(
|
export const Button = React.forwardRef(
|
||||||
|
46
pkg/grid/src/components/ProviderLink.tsx
Normal file
46
pkg/grid/src/components/ProviderLink.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link, LinkProps } from 'react-router-dom';
|
||||||
|
import { Provider } from '@urbit/api';
|
||||||
|
import { ShipName } from './ShipName';
|
||||||
|
|
||||||
|
export type ProviderLinkProps = Omit<LinkProps, 'to'> & {
|
||||||
|
provider: Provider;
|
||||||
|
small?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
|
to?: (p: Provider) => LinkProps['to'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProviderLink = ({
|
||||||
|
provider,
|
||||||
|
to,
|
||||||
|
selected = false,
|
||||||
|
small = false,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ProviderLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={(to && to(provider)) || `/leap/search/${provider.shipName}/apps`}
|
||||||
|
className={classNames(
|
||||||
|
'flex items-center space-x-3 default-ring ring-offset-2 rounded-lg',
|
||||||
|
selected && 'ring-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'flex-none relative bg-black rounded-lg',
|
||||||
|
small ? 'w-8 h-8' : 'w-12 h-12'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* TODO: Handle sigils */}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-black">
|
||||||
|
<p className="font-mono">{provider.nickname || <ShipName name={provider.shipName} />}</p>
|
||||||
|
{provider.status && !small && <p className="font-normal">{provider.status}</p>}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
55
pkg/grid/src/components/ProviderList.tsx
Normal file
55
pkg/grid/src/components/ProviderList.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React, { MouseEvent, useCallback } from 'react';
|
||||||
|
import { Provider } from '@urbit/api';
|
||||||
|
import { MatchItem } from '../nav/Nav';
|
||||||
|
import { useRecentsStore } from '../nav/search/Home';
|
||||||
|
import { ProviderLink, ProviderLinkProps } from './ProviderLink';
|
||||||
|
|
||||||
|
export type ProviderListProps = {
|
||||||
|
providers: Provider[];
|
||||||
|
labelledBy: string;
|
||||||
|
matchAgainst?: MatchItem;
|
||||||
|
onClick?: (e: MouseEvent<HTMLAnchorElement>, p: Provider) => void;
|
||||||
|
} & Omit<ProviderLinkProps, 'provider' | 'onClick'>;
|
||||||
|
|
||||||
|
export function providerMatches(target: Provider, match?: MatchItem): boolean {
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchValue = match.display || match.value;
|
||||||
|
return target.nickname === matchValue || target.shipName === matchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProviderList = ({
|
||||||
|
providers,
|
||||||
|
labelledBy,
|
||||||
|
matchAgainst,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: ProviderListProps) => {
|
||||||
|
const addRecentDev = useRecentsStore((state) => state.addRecentDev);
|
||||||
|
const selected = useCallback(
|
||||||
|
(provider: Provider) => providerMatches(provider, matchAgainst),
|
||||||
|
[matchAgainst]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="space-y-8" aria-labelledby={labelledBy}>
|
||||||
|
{providers.map((p) => (
|
||||||
|
<li key={p.shipName} id={p.shipName} role="option" aria-selected={selected(p)}>
|
||||||
|
<ProviderLink
|
||||||
|
{...props}
|
||||||
|
provider={p}
|
||||||
|
selected={selected(p)}
|
||||||
|
onClick={(e) => {
|
||||||
|
addRecentDev(p);
|
||||||
|
if (onClick) {
|
||||||
|
onClick(e, p);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
@ -5,16 +5,20 @@ type ShipNameProps = {
|
|||||||
} & HTMLAttributes<HTMLSpanElement>;
|
} & HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
export const ShipName = ({ name, ...props }: ShipNameProps) => {
|
export const ShipName = ({ name, ...props }: ShipNameProps) => {
|
||||||
const parts = name.replace('~', '').split(/[_^-]/);
|
const parts = name.replace('~', '').split(/([_^-])/);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span {...props}>
|
<span {...props}>
|
||||||
<span aria-hidden>~</span>
|
<span aria-hidden>~</span>
|
||||||
{/* <span className="sr-only">sig</span> */}
|
{/* <span className="sr-only">sig</span> */}
|
||||||
<span>{parts[0]}</span>
|
<span>{parts[0]}</span>
|
||||||
<span aria-hidden>-</span>
|
{parts.length > 1 && (
|
||||||
{/* <span className="sr-only">hep</span> */}
|
<>
|
||||||
<span>{parts[1]}</span>
|
<span aria-hidden>{parts[1]}</span>
|
||||||
|
{/* <span className="sr-only">hep</span> */}
|
||||||
|
<span>{parts[2]}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
14
pkg/grid/src/components/icons/Elbow.tsx
Normal file
14
pkg/grid/src/components/icons/Elbow.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React, { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
type ElbowProps = HTMLAttributes<SVGSVGElement>;
|
||||||
|
|
||||||
|
export const Elbow = (props: ElbowProps) => (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path
|
||||||
|
d="M11 1V5C11 9.41828 14.5817 13 19 13H23"
|
||||||
|
className="stroke-current"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
@ -136,7 +136,7 @@ export const Leap = React.forwardRef(({ menu, dropdown, showClose, className }:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = [slugify(getMatch(value)?.value || value)];
|
const input = [getMatch(value)?.value || slugify(value)];
|
||||||
if (appsMatch) {
|
if (appsMatch) {
|
||||||
input.unshift(match?.params.query || '');
|
input.unshift(match?.params.query || '');
|
||||||
} else {
|
} else {
|
||||||
|
@ -85,11 +85,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
|||||||
const dialogNavRef = useRef<HTMLDivElement>(null);
|
const dialogNavRef = useRef<HTMLDivElement>(null);
|
||||||
const [systemMenuOpen, setSystemMenuOpen] = useState(false);
|
const [systemMenuOpen, setSystemMenuOpen] = useState(false);
|
||||||
const [dialogContentOpen, setDialogContentOpen] = useState(false);
|
const [dialogContentOpen, setDialogContentOpen] = useState(false);
|
||||||
const { selection, select } = useLeapStore((state) => ({
|
const select = useLeapStore((state) => state.select);
|
||||||
selectedMatch: state.selectedMatch,
|
|
||||||
selection: state.selection,
|
|
||||||
select: state.select
|
|
||||||
}));
|
|
||||||
|
|
||||||
const menuState = menu || 'closed';
|
const menuState = menu || 'closed';
|
||||||
const isOpen = menuState !== 'closed';
|
const isOpen = menuState !== 'closed';
|
||||||
@ -99,10 +95,8 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
|||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
select(null);
|
select(null);
|
||||||
setDialogContentOpen(false);
|
setDialogContentOpen(false);
|
||||||
} else {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}
|
}
|
||||||
}, [selection, isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const onOpen = useCallback(
|
const onOpen = useCallback(
|
||||||
(event: Event) => {
|
(event: Event) => {
|
||||||
@ -124,6 +118,15 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const disableCloseWhenDropdownOpen = useCallback(
|
||||||
|
(e: Event) => {
|
||||||
|
if (systemMenuOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[systemMenuOpen]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Portal.Root
|
<Portal.Root
|
||||||
@ -133,6 +136,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
|||||||
<SystemMenu
|
<SystemMenu
|
||||||
open={systemMenuOpen}
|
open={systemMenuOpen}
|
||||||
setOpen={setSystemMenuOpen}
|
setOpen={setSystemMenuOpen}
|
||||||
|
showOverlay={!isOpen}
|
||||||
className={classNames('relative z-50 flex-none', eitherOpen ? 'bg-white' : 'bg-gray-100')}
|
className={classNames('relative z-50 flex-none', eitherOpen ? 'bg-white' : 'bg-gray-100')}
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
@ -163,13 +167,14 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
|||||||
<Dialog open={isOpen} onOpenChange={onDialogClose}>
|
<Dialog open={isOpen} onOpenChange={onDialogClose}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
onOpenAutoFocus={onOpen}
|
onOpenAutoFocus={onOpen}
|
||||||
className="fixed top-0 left-[calc(50%)] w-[calc(100%-15px)] max-w-3xl px-4 text-gray-400 -translate-x-1/2 outline-none"
|
onInteractOutside={disableCloseWhenDropdownOpen}
|
||||||
|
className="fixed bottom-0 sm:top-0 scroll-left-50 flex flex-col scroll-full-width max-w-3xl px-4 text-gray-400 -translate-x-1/2 outline-none"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-controls="leap-items"
|
aria-controls="leap-items"
|
||||||
aria-owns="leap-items"
|
aria-owns="leap-items"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
>
|
>
|
||||||
<header ref={dialogNavRef} className="my-6" />
|
<header ref={dialogNavRef} className="my-6 order-last sm:order-none" />
|
||||||
<div
|
<div
|
||||||
id="leap-items"
|
id="leap-items"
|
||||||
className="grid grid-rows-[fit-content(calc(100vh-7.5rem))] bg-white rounded-3xl overflow-hidden default-ring"
|
className="grid grid-rows-[fit-content(calc(100vh-7.5rem))] bg-white rounded-3xl overflow-hidden default-ring"
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import React, { FC, useEffect, useState } from 'react';
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { useLeapStore } from './Nav';
|
import { useLeapStore } from './Nav';
|
||||||
import { useBlockers, useLag } from '../state/kiln';
|
import { useBlockers, useLag } from '../state/kiln';
|
||||||
import { useCharge } from '../state/docket';
|
import { useCharge } from '../state/docket';
|
||||||
import { DocketImage } from '../components/DocketImage';
|
import { DocketImage } from '../components/DocketImage';
|
||||||
import api from '../state/api';
|
import api from '../state/api';
|
||||||
import { kilnSuspend } from '../../../npm/api/hood';
|
import { kilnSuspend } from '../../../npm/api/hood';
|
||||||
|
import { Button } from '../components/Button';
|
||||||
|
import { useHarkStore } from '../state/hark';
|
||||||
|
import { Notification } from '../state/hark-types';
|
||||||
|
import { BasicNotification } from './notifications/BasicNotification';
|
||||||
|
import { SystemNotification } from './notifications/SystemNotification';
|
||||||
|
|
||||||
interface INotification {
|
interface INotification {
|
||||||
title: string;
|
title: string;
|
||||||
@ -24,7 +30,7 @@ interface NotificationProps {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Notification: FC<NotificationProps> = ({ notification, className, children }) => (
|
const Notification: FC<NotificationProps> = ({ className, children }) => (
|
||||||
<div className={cn('rounded-md flex flex-col p-4', className)}>{children}</div>
|
<div className={cn('rounded-md flex flex-col p-4', className)}>{children}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -78,15 +84,35 @@ const BlockNotification: React.FC<BlockNotificationProps> = ({ desks }) => {
|
|||||||
provides an app update.
|
provides an app update.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
<button onClick={() => setDismissed(true)}>Dismiss</button>
|
<button type="button" onClick={() => setDismissed(true)}>
|
||||||
<button onClick={onArchive}>Archive {count} apps and System Update</button>
|
Dismiss
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onArchive}>
|
||||||
|
Archive {count} apps and System Update
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Notification>
|
</Notification>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function renderNotification(notification: Notification, key: string) {
|
||||||
|
if (notification.type === 'system-updates-blocked') {
|
||||||
|
return <SystemNotification key={key} notification={notification} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BasicNotification key={key} notification={notification} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Empty = () => (
|
||||||
|
<section className="flex justify-center items-center min-h-[480px] text-gray-400 space-y-2">
|
||||||
|
<span className="h4">All clear!</span>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
export const Notifications = () => {
|
export const Notifications = () => {
|
||||||
const select = useLeapStore((state) => state.select);
|
const select = useLeapStore((s) => s.select);
|
||||||
|
const notifications = useHarkStore((s) => s.notifications);
|
||||||
|
const hasNotifications = notifications.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
select('Notifications');
|
select('Notifications');
|
||||||
@ -96,15 +122,29 @@ export const Notifications = () => {
|
|||||||
const lag = useLag();
|
const lag = useLag();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 md:p-8 space-y-8">
|
<div className="p-4 md:p-8">
|
||||||
{lag && <LagNotification />}
|
{lag && <LagNotification />}
|
||||||
{blockers.length > 0 ? <BlockNotification desks={blockers} /> : null}
|
{blockers.length > 0 ? <BlockNotification desks={blockers} /> : null}
|
||||||
{/*<h2 className="h4 text-gray-500">Recent Apps</h2>
|
<header className="space-x-2 mb-8">
|
||||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
<Button variant="secondary" className="py-1.5 px-6 rounded-full">
|
||||||
<hr className="-mx-4 md:-mx-8" />
|
Mark All as Read
|
||||||
<h2 className="h4 text-gray-500">Recent Developers</h2>
|
</Button>
|
||||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
<Button
|
||||||
*/}
|
as={Link}
|
||||||
|
variant="secondary"
|
||||||
|
to="/leap/system-preferences/notifications"
|
||||||
|
className="py-1.5 px-6 rounded-full"
|
||||||
|
>
|
||||||
|
Notification Settings
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{!hasNotifications && <Empty />}
|
||||||
|
{hasNotifications && (
|
||||||
|
<section className="min-h-[480px] text-gray-400 space-y-2">
|
||||||
|
{notifications.map((n, index) => renderNotification(n, index.toString()))}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
10
pkg/grid/src/nav/notifications/BasicNotification.tsx
Normal file
10
pkg/grid/src/nav/notifications/BasicNotification.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BasicNotification as BasicNotificationType } from '../../state/hark-types';
|
||||||
|
|
||||||
|
interface BasicNotificationProps {
|
||||||
|
notification: BasicNotificationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BasicNotification = ({ notification }: BasicNotificationProps) => (
|
||||||
|
<div>{notification.message}</div>
|
||||||
|
);
|
36
pkg/grid/src/nav/notifications/NotificationButton.tsx
Normal file
36
pkg/grid/src/nav/notifications/NotificationButton.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type * as Polymorphic from '@radix-ui/react-polymorphic';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
type NotificationButtonVariant = 'primary' | 'secondary' | 'destructive';
|
||||||
|
|
||||||
|
type PolymorphicButton = Polymorphic.ForwardRefComponent<
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
variant?: NotificationButtonVariant;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
const variants: Record<NotificationButtonVariant, string> = {
|
||||||
|
primary: 'text-blue bg-white',
|
||||||
|
secondary: 'text-black bg-white',
|
||||||
|
destructive: 'text-red-400 bg-white'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotificationButton = React.forwardRef(
|
||||||
|
({ as: Comp = 'button', variant = 'primary', children, className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={classNames(
|
||||||
|
'button p-1 leading-4 font-medium default-ring rounded',
|
||||||
|
variants[variant],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
) as PolymorphicButton;
|
121
pkg/grid/src/nav/notifications/SystemNotification.tsx
Normal file
121
pkg/grid/src/nav/notifications/SystemNotification.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { pick } from 'lodash-es';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { AppList } from '../../components/AppList';
|
||||||
|
import { Button } from '../../components/Button';
|
||||||
|
import { Dialog, DialogClose, DialogContent, DialogTrigger } from '../../components/Dialog';
|
||||||
|
import { Elbow } from '../../components/icons/Elbow';
|
||||||
|
import { useCharges } from '../../state/docket';
|
||||||
|
import { SystemNotification as SystemNotificationType } from '../../state/hark-types';
|
||||||
|
import { NotificationButton } from './NotificationButton';
|
||||||
|
|
||||||
|
interface SystemNotificationProps {
|
||||||
|
notification: SystemNotificationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SystemNotification = ({ notification }: SystemNotificationProps) => {
|
||||||
|
const keys = notification.charges;
|
||||||
|
const charges = useCharges();
|
||||||
|
const blockedCharges = Object.values(pick(charges, keys));
|
||||||
|
const count = blockedCharges.length;
|
||||||
|
|
||||||
|
const handlePauseOTAs = useCallback(() => {
|
||||||
|
console.log('pause updates');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleArchiveApps = useCallback(() => {
|
||||||
|
console.log('archive apps');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="notification pl-12 space-y-2 text-black bg-orange-50"
|
||||||
|
aria-labelledby="system-updates-blocked"
|
||||||
|
>
|
||||||
|
<header id="system-updates-blocked" className="relative -left-8 space-y-2">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<span className="inline-block w-6 h-6 bg-orange-500 rounded-full" />
|
||||||
|
<span className="font-medium">Landscape</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Elbow className="w-6 h-6 text-gray-300" />
|
||||||
|
<h2 id="blocked-apps">The following ({count}) apps blocked a System Update:</h2>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<AppList
|
||||||
|
apps={blockedCharges}
|
||||||
|
labelledBy="blocked-apps"
|
||||||
|
size="xs"
|
||||||
|
className="font-medium"
|
||||||
|
listClass="space-y-2"
|
||||||
|
/>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<p>
|
||||||
|
In order to proceed with the System Update, you’ll need to temporarily archive these apps,
|
||||||
|
which will render them unusable, but with data intact.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Archived apps will automatically un-archive and resume operation when their developer
|
||||||
|
provides an app update.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger as={NotificationButton}>Dismiss</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
showClose={false}
|
||||||
|
className="max-w-[400px] space-y-6 text-base tracking-tight"
|
||||||
|
>
|
||||||
|
<h2 className="h4">Skip System Update</h2>
|
||||||
|
<p>
|
||||||
|
Skipping the application fo an incoming System Update will grant you the ability to
|
||||||
|
continue using incompatible apps at the cost of an urbit that's not up to date.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can choose to apply System Updates from System Preferences any time.{' '}
|
||||||
|
<a href="https://tlon.io" target="_blank" rel="noreferrer">
|
||||||
|
Learn More
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<div className="flex space-x-6">
|
||||||
|
<DialogClose as={Button} variant="secondary">
|
||||||
|
Cancel
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose as={Button} variant="caution" onClick={handlePauseOTAs}>
|
||||||
|
Pause OTAs
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger as={NotificationButton}>
|
||||||
|
Archive ({count}) apps and Apply System Update
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
showClose={false}
|
||||||
|
className="max-w-[400px] space-y-6 text-base tracking-tight"
|
||||||
|
>
|
||||||
|
<h2 className="h4">Archive ({count}) Apps and Apply System Update</h2>
|
||||||
|
<p>
|
||||||
|
The following apps will be archived until their developer provides a compatible update
|
||||||
|
to your system.
|
||||||
|
</p>
|
||||||
|
<AppList
|
||||||
|
apps={blockedCharges}
|
||||||
|
labelledBy="blocked-apps"
|
||||||
|
size="xs"
|
||||||
|
listClass="space-y-2"
|
||||||
|
/>
|
||||||
|
<div className="flex space-x-6">
|
||||||
|
<DialogClose as={Button} variant="secondary">
|
||||||
|
Cancel
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose as={Button} variant="caution" onClick={handleArchiveApps}>
|
||||||
|
Archive Apps
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
@ -2,7 +2,8 @@ import { chadIsRunning } from '@urbit/api/docket';
|
|||||||
import clipboardCopy from 'clipboard-copy';
|
import clipboardCopy from 'clipboard-copy';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { PillButton } from '../../components/Button';
|
import { Button, PillButton } from '../../components/Button';
|
||||||
|
import { Dialog, DialogClose, DialogContent, DialogTrigger } from '../../components/Dialog';
|
||||||
import { DocketHeader } from '../../components/DocketHeader';
|
import { DocketHeader } from '../../components/DocketHeader';
|
||||||
import { ShipName } from '../../components/ShipName';
|
import { ShipName } from '../../components/ShipName';
|
||||||
import { Spinner } from '../../components/Spinner';
|
import { Spinner } from '../../components/Spinner';
|
||||||
@ -10,8 +11,10 @@ import { TreatyMeta } from '../../components/TreatyMeta';
|
|||||||
import useDocketState, { useCharges, useTreaty } from '../../state/docket';
|
import useDocketState, { useCharges, useTreaty } from '../../state/docket';
|
||||||
import { getAppHref } from '../../state/util';
|
import { getAppHref } from '../../state/util';
|
||||||
import { useLeapStore } from '../Nav';
|
import { useLeapStore } from '../Nav';
|
||||||
|
import { useRecentsStore } from './Home';
|
||||||
|
|
||||||
export const AppInfo = () => {
|
export const AppInfo = () => {
|
||||||
|
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
||||||
const select = useLeapStore((state) => state.select);
|
const select = useLeapStore((state) => state.select);
|
||||||
const { ship, host, desk } = useParams<{ ship: string; host: string; desk: string }>();
|
const { ship, host, desk } = useParams<{ ship: string; host: string; desk: string }>();
|
||||||
const treaty = useTreaty(host, desk);
|
const treaty = useTreaty(host, desk);
|
||||||
@ -49,23 +52,46 @@ export const AppInfo = () => {
|
|||||||
<DocketHeader docket={treaty}>
|
<DocketHeader docket={treaty}>
|
||||||
<div className="col-span-2 md:col-span-1 flex items-center space-x-4">
|
<div className="col-span-2 md:col-span-1 flex items-center space-x-4">
|
||||||
{installed && (
|
{installed && (
|
||||||
<PillButton as="a" href={getAppHref(treaty.href)} target={treaty.title || '_blank'}>
|
<PillButton
|
||||||
|
variant="alt-primary"
|
||||||
|
as="a"
|
||||||
|
href={getAppHref(treaty.href)}
|
||||||
|
target={treaty.title || '_blank'}
|
||||||
|
onClick={() => addRecentApp(treaty)}
|
||||||
|
>
|
||||||
Open App
|
Open App
|
||||||
</PillButton>
|
</PillButton>
|
||||||
)}
|
)}
|
||||||
{!installed && (
|
{!installed && (
|
||||||
<PillButton onClick={installApp}>
|
<Dialog>
|
||||||
{installing ? (
|
<DialogTrigger as={PillButton} variant="alt-primary">
|
||||||
<>
|
{installing ? (
|
||||||
<Spinner />
|
<>
|
||||||
<span className="sr-only">Installing...</span>
|
<Spinner />
|
||||||
</>
|
<span className="sr-only">Installing...</span>
|
||||||
) : (
|
</>
|
||||||
'Get App'
|
) : (
|
||||||
)}
|
'Get App'
|
||||||
</PillButton>
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent showClose={false} className="max-w-[400px] space-y-6">
|
||||||
|
<h2 className="h4">Install “{treaty.title}”</h2>
|
||||||
|
<p className="text-base tracking-tight pr-6">
|
||||||
|
This application will be able to view and interact with the contents of your
|
||||||
|
Urbit. Only install if you trust the developer.
|
||||||
|
</p>
|
||||||
|
<div className="flex space-x-6">
|
||||||
|
<DialogClose as={Button} variant="secondary">
|
||||||
|
Cancel
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose as={Button} onClick={installApp}>
|
||||||
|
Get “{treaty.title}”
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
<PillButton variant="secondary" onClick={copyApp}>
|
<PillButton variant="alt-secondary" onClick={copyApp}>
|
||||||
Copy App Link
|
Copy App Link
|
||||||
</PillButton>
|
</PillButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import fuzzy from 'fuzzy';
|
import fuzzy from 'fuzzy';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
import classNames from 'classnames';
|
import { Docket } from '@urbit/api/docket';
|
||||||
import { Treaty } from '@urbit/api/docket';
|
|
||||||
import { ShipName } from '../../components/ShipName';
|
import { ShipName } from '../../components/ShipName';
|
||||||
import useDocketState, { useAllyTreaties } from '../../state/docket';
|
import useDocketState, { useAllyTreaties } from '../../state/docket';
|
||||||
import { useLeapStore } from '../Nav';
|
import { MatchItem, useLeapStore } from '../Nav';
|
||||||
|
import { AppList } from '../../components/AppList';
|
||||||
|
|
||||||
type AppsProps = RouteComponentProps<{ ship: string }>;
|
type AppsProps = RouteComponentProps<{ ship: string }>;
|
||||||
|
|
||||||
|
export function appMatch(app: Docket): MatchItem {
|
||||||
|
// TODO: do we need display vs value here,
|
||||||
|
// will all apps have unique titles? If not,
|
||||||
|
// what would we use?
|
||||||
|
return { value: app.title, display: app.title };
|
||||||
|
}
|
||||||
|
|
||||||
export const Apps = ({ match }: AppsProps) => {
|
export const Apps = ({ match }: AppsProps) => {
|
||||||
const { searchInput, selectedMatch, select } = useLeapStore((state) => ({
|
const { searchInput, selectedMatch, select } = useLeapStore((state) => ({
|
||||||
searchInput: state.searchInput,
|
searchInput: state.searchInput,
|
||||||
@ -49,7 +56,7 @@ export const Apps = ({ match }: AppsProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (results) {
|
if (results) {
|
||||||
useLeapStore.setState({
|
useLeapStore.setState({
|
||||||
matches: results.map((treaty) => ({ value: treaty.desk, display: treaty.title }))
|
matches: results.map(appMatch)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [results]);
|
}, [results]);
|
||||||
@ -60,18 +67,6 @@ export const Apps = ({ match }: AppsProps) => {
|
|||||||
}
|
}
|
||||||
}, [provider]);
|
}, [provider]);
|
||||||
|
|
||||||
const isSelected = useCallback(
|
|
||||||
(target: Treaty) => {
|
|
||||||
if (!selectedMatch) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchValue = selectedMatch.display || selectedMatch.value;
|
|
||||||
return target.title === matchValue || target.desk === matchValue;
|
|
||||||
},
|
|
||||||
[selectedMatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400">
|
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400">
|
||||||
<div id="developed-by">
|
<div id="developed-by">
|
||||||
@ -83,43 +78,12 @@ export const Apps = ({ match }: AppsProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{results && (
|
{results && (
|
||||||
<ul className="space-y-8" aria-labelledby="developed-by">
|
<AppList
|
||||||
{results.map((treaty) => (
|
apps={results}
|
||||||
<li
|
labelledBy="developed-by"
|
||||||
key={treaty.desk}
|
matchAgainst={selectedMatch}
|
||||||
id={treaty.title || treaty.desk}
|
to={(app) => `${match?.path.replace(':ship', provider)}/${slugify(app.base)}`}
|
||||||
role="option"
|
/>
|
||||||
aria-selected={isSelected(treaty)}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
to={`${match?.path.replace(':ship', provider)}/${treaty.ship}/${slugify(
|
|
||||||
treaty.desk
|
|
||||||
)}`}
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center space-x-3 default-ring ring-offset-2 rounded-lg',
|
|
||||||
isSelected(treaty) && 'ring-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="flex-none relative w-12 h-12 bg-gray-200 rounded-lg"
|
|
||||||
style={{ backgroundColor: treaty.color }}
|
|
||||||
>
|
|
||||||
{treaty.image && (
|
|
||||||
<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={treaty.image}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-black">
|
|
||||||
<p>{treaty.title}</p>
|
|
||||||
{treaty.info && <p className="font-normal">{treaty.info}</p>}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
)}
|
||||||
<p>That's it!</p>
|
<p>That's it!</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,35 +1,115 @@
|
|||||||
import { debounce } from 'lodash-es';
|
import produce from 'immer';
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import create from 'zustand';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import React, { useEffect } from 'react';
|
||||||
import { createNextPath, useLeapStore } from '../Nav';
|
import { persist } from 'zustand/middleware';
|
||||||
|
import { take } from 'lodash-es';
|
||||||
|
import { Docket, Provider } from '@urbit/api';
|
||||||
|
import { MatchItem, useLeapStore } from '../Nav';
|
||||||
|
import { appMatch } from './Apps';
|
||||||
|
import { providerMatch } from './Providers';
|
||||||
|
import { AppList } from '../../components/AppList';
|
||||||
|
import { ProviderList } from '../../components/ProviderList';
|
||||||
|
import { AppLink } from '../../components/AppLink';
|
||||||
|
import { ShipName } from '../../components/ShipName';
|
||||||
|
import { ProviderLink } from '../../components/ProviderLink';
|
||||||
|
import { useCharges } from '../../state/docket';
|
||||||
|
|
||||||
type HomeProps = RouteComponentProps;
|
interface RecentsStore {
|
||||||
|
recentApps: Docket[];
|
||||||
|
recentDevs: Provider[];
|
||||||
|
addRecentApp: (docket: Docket) => void;
|
||||||
|
addRecentDev: (dev: Provider) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const Home = ({ match, history }: HomeProps) => {
|
export const useRecentsStore = create<RecentsStore>(
|
||||||
const searchInput = useLeapStore((s) => s.searchInput);
|
persist(
|
||||||
const { push } = history;
|
(set) => ({
|
||||||
const { path } = match;
|
recentApps: [],
|
||||||
|
recentDevs: [],
|
||||||
|
addRecentApp: (docket) => {
|
||||||
|
set(
|
||||||
|
produce((draft: RecentsStore) => {
|
||||||
|
const hasApp = draft.recentApps.find((app) => app.href === docket.href);
|
||||||
|
if (!hasApp) {
|
||||||
|
draft.recentApps.unshift(docket);
|
||||||
|
}
|
||||||
|
|
||||||
const handleSearch = useCallback(
|
draft.recentApps = take(draft.recentApps, 3);
|
||||||
debounce((input: string) => {
|
})
|
||||||
push(createNextPath(path, input.trim()));
|
);
|
||||||
}, 300),
|
},
|
||||||
[path]
|
addRecentDev: (dev) => {
|
||||||
);
|
set(
|
||||||
|
produce((draft: RecentsStore) => {
|
||||||
|
const hasDev = draft.recentDevs.find((p) => p.shipName === dev.shipName);
|
||||||
|
if (!hasDev) {
|
||||||
|
draft.recentDevs.unshift(dev);
|
||||||
|
}
|
||||||
|
|
||||||
|
draft.recentDevs = take(draft.recentDevs, 3);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
whitelist: ['recentApps', 'recentDevs'],
|
||||||
|
name: 'recents-store'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Home = () => {
|
||||||
|
const selectedMatch = useLeapStore((state) => state.selectedMatch);
|
||||||
|
const { recentApps, recentDevs, addRecentApp, addRecentDev } = useRecentsStore();
|
||||||
|
const charges = useCharges();
|
||||||
|
const groups = charges?.groups;
|
||||||
|
const zod = { shipName: '~zod' };
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchInput) {
|
const apps = recentApps.map(appMatch);
|
||||||
handleSearch(searchInput);
|
const devs = recentDevs.map(providerMatch);
|
||||||
}
|
|
||||||
}, [searchInput]);
|
useLeapStore.setState({
|
||||||
|
matches: ([] as MatchItem[]).concat(apps, devs)
|
||||||
|
});
|
||||||
|
}, [recentApps, recentDevs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full p-4 md:p-8 space-y-8 overflow-y-auto">
|
<div className="h-full p-4 md:p-8 space-y-8 font-semibold leading-tight text-black overflow-y-auto">
|
||||||
<h2 className="h4 text-gray-500">Recent Apps</h2>
|
<h2 id="recent-apps" className="h4 text-gray-500">
|
||||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
Recent Apps
|
||||||
|
</h2>
|
||||||
|
{recentApps.length === 0 && (
|
||||||
|
<div className="min-h-[150px] p-6 rounded-xl bg-gray-100">
|
||||||
|
<p className="mb-4">Apps you use will be listed here, in the order you used them.</p>
|
||||||
|
<p className="mb-6">You can click/tap/keyboard on a listed app to open it.</p>
|
||||||
|
{groups && <AppLink app={groups} small onClick={() => addRecentApp(groups)} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{recentApps.length > 0 && (
|
||||||
|
<AppList apps={recentApps} labelledBy="recent-apps" matchAgainst={selectedMatch} small />
|
||||||
|
)}
|
||||||
<hr className="-mx-4 md:-mx-8" />
|
<hr className="-mx-4 md:-mx-8" />
|
||||||
<h2 className="h4 text-gray-500">Recent Developers</h2>
|
<h2 id="recent-devs" className="h4 text-gray-500">
|
||||||
<div className="min-h-[150px] rounded-xl bg-gray-100" />
|
Recent Developers
|
||||||
|
</h2>
|
||||||
|
{recentDevs.length === 0 && (
|
||||||
|
<div className="min-h-[150px] p-6 rounded-xl bg-gray-100">
|
||||||
|
<p className="mb-4">Urbit app developers you search for will be listed here.</p>
|
||||||
|
<p className="mb-6">
|
||||||
|
Try out app discovery by visiting <ShipName name="~zod" /> below.
|
||||||
|
</p>
|
||||||
|
<ProviderLink provider={zod} small onClick={() => addRecentDev(zod)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{recentDevs.length > 0 && (
|
||||||
|
<ProviderList
|
||||||
|
providers={recentDevs}
|
||||||
|
labelledBy="recent-devs"
|
||||||
|
matchAgainst={selectedMatch}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,21 @@
|
|||||||
import classNames from 'classnames';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
|
||||||
import fuzzy from 'fuzzy';
|
import fuzzy from 'fuzzy';
|
||||||
import { ShipName } from '../../components/ShipName';
|
import { Provider } from '@urbit/api';
|
||||||
import { useLeapStore } from '../Nav';
|
import { MatchItem, useLeapStore } from '../Nav';
|
||||||
import { useAllies } from '../../state/docket';
|
import { useAllies } from '../../state/docket';
|
||||||
|
import { ProviderList } from '../../components/ProviderList';
|
||||||
|
|
||||||
type ProvidersProps = RouteComponentProps<{ ship: string }>;
|
type ProvidersProps = RouteComponentProps<{ ship: string }>;
|
||||||
|
|
||||||
|
export function providerMatch(provider: Provider | string): MatchItem {
|
||||||
|
if (typeof provider === 'string') {
|
||||||
|
return { value: provider, display: provider };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value: provider.shipName, display: provider.nickname };
|
||||||
|
}
|
||||||
|
|
||||||
export const Providers = ({ match }: ProvidersProps) => {
|
export const Providers = ({ match }: ProvidersProps) => {
|
||||||
const { selectedMatch, select } = useLeapStore((state) => ({
|
const { selectedMatch, select } = useLeapStore((state) => ({
|
||||||
select: state.select,
|
select: state.select,
|
||||||
@ -30,10 +38,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
|||||||
|
|
||||||
return right - left;
|
return right - left;
|
||||||
})
|
})
|
||||||
.map((el) => {
|
.map((el) => ({ shipName: el.original }))
|
||||||
console.log(el);
|
|
||||||
return el.original;
|
|
||||||
})
|
|
||||||
: [],
|
: [],
|
||||||
[allies, search]
|
[allies, search]
|
||||||
);
|
);
|
||||||
@ -47,23 +52,11 @@ export const Providers = ({ match }: ProvidersProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (results) {
|
if (results) {
|
||||||
useLeapStore.setState({
|
useLeapStore.setState({
|
||||||
matches: results.map((p) => ({ value: p, display: p }))
|
matches: results.map(providerMatch)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [results]);
|
}, [results]);
|
||||||
|
|
||||||
const isSelected = useCallback(
|
|
||||||
(target: string) => {
|
|
||||||
if (!selectedMatch) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchValue = selectedMatch.display || selectedMatch.value;
|
|
||||||
return target === matchValue;
|
|
||||||
},
|
|
||||||
[selectedMatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400" aria-live="polite">
|
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400" aria-live="polite">
|
||||||
<div id="providers">
|
<div id="providers">
|
||||||
@ -73,28 +66,7 @@ export const Providers = ({ match }: ProvidersProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{results && (
|
{results && (
|
||||||
<ul className="space-y-8" aria-labelledby="providers">
|
<ProviderList providers={results} labelledBy="providers" matchAgainst={selectedMatch} />
|
||||||
{results.map((p) => (
|
|
||||||
<li key={p} id={p} role="option" aria-selected={isSelected(p)}>
|
|
||||||
<Link
|
|
||||||
to={`${match?.path.replace(':ship', p)}/apps`}
|
|
||||||
className={classNames(
|
|
||||||
'flex items-center space-x-3 default-ring ring-offset-2 rounded-lg',
|
|
||||||
isSelected(p) && 'ring-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<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">
|
|
||||||
<ShipName name={p} />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
)}
|
||||||
<p>That's it!</p>
|
<p>That's it!</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,25 +25,34 @@ export const Grid: FunctionComponent<GridProps> = ({ match }) => {
|
|||||||
fetchAllies();
|
fetchAllies();
|
||||||
fetchVats();
|
fetchVats();
|
||||||
fetchLag();
|
fetchLag();
|
||||||
api.subscribe({ app: 'hood', path: '/kiln/vats', event: (data: KilnDiff) => {
|
api
|
||||||
console.log(data);
|
.subscribe({
|
||||||
}, err: () => { }, quit: () => {} }).catch(e => {
|
app: 'hood',
|
||||||
console.log(e);
|
path: '/kiln/vats',
|
||||||
|
event: (data: KilnDiff) => {
|
||||||
}).then(r => { console.log(r); });
|
console.log(data);
|
||||||
|
},
|
||||||
|
err: () => {},
|
||||||
|
quit: () => {}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
console.log(r);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<header className="sticky top-0 left-0 z-30 flex justify-center w-full bg-white">
|
<header className="fixed sm:sticky bottom-0 sm:bottom-auto sm:top-0 left-0 z-30 flex justify-center w-full bg-white">
|
||||||
<Nav menu={match.params.menu} />
|
<Nav menu={match.params.menu} />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="h-full w-full flex justify-center pt-24 pb-32 relative z-0">
|
<main className="h-full w-full flex justify-center pt-4 md:pt-16 pb-32 relative z-0">
|
||||||
{!chargesLoaded && <span>Loading...</span>}
|
{!chargesLoaded && <span>Loading...</span>}
|
||||||
{chargesLoaded && (
|
{chargesLoaded && (
|
||||||
<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">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 px-4 md:px-8 w-full max-w-6xl">
|
||||||
{charges &&
|
{charges &&
|
||||||
map(omit(charges, 'grid'), (charge, desk) => (
|
map(omit(charges, 'grid'), (charge, desk) => (
|
||||||
<Tile key={desk} charge={charge} desk={desk} />
|
<Tile key={desk} charge={charge} desk={desk} />
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import create from 'zustand';
|
import create from 'zustand';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { omit } from 'lodash-es';
|
import { mapValues, omit, pick } from 'lodash-es';
|
||||||
import api from './api';
|
|
||||||
import { mockAllies, mockCharges, mockTreaties } from './mock-data';
|
|
||||||
import {
|
import {
|
||||||
Allies,
|
Allies,
|
||||||
Charge,
|
Charge,
|
||||||
@ -21,8 +19,10 @@ import {
|
|||||||
docketInstall,
|
docketInstall,
|
||||||
ChargeUpdate
|
ChargeUpdate
|
||||||
} from '@urbit/api/docket';
|
} from '@urbit/api/docket';
|
||||||
import _ from 'lodash';
|
import { kilnRevive, kilnSuspend } from '@urbit/api/hood';
|
||||||
import {kilnRevive, kilnSuspend} from '@urbit/api/hood';
|
import api from './api';
|
||||||
|
import { mockAllies, mockCharges, mockTreaties } from './mock-data';
|
||||||
|
import { fakeRequest } from './util';
|
||||||
|
|
||||||
const useMockData = import.meta.env.MODE === 'mock';
|
const useMockData = import.meta.env.MODE === 'mock';
|
||||||
|
|
||||||
@ -39,14 +39,6 @@ interface DocketState {
|
|||||||
uninstallDocket: (desk: string) => Promise<number | void>;
|
uninstallDocket: (desk: string) => Promise<number | void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fakeRequest<T>(data: T, time = 300): Promise<T> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(data);
|
|
||||||
}, time);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const useDocketState = create<DocketState>((set, get) => ({
|
const useDocketState = create<DocketState>((set, get) => ({
|
||||||
fetchCharges: async () => {
|
fetchCharges: async () => {
|
||||||
const charg = useMockData
|
const charg = useMockData
|
||||||
@ -67,8 +59,10 @@ const useDocketState = create<DocketState>((set, get) => ({
|
|||||||
return allies;
|
return allies;
|
||||||
},
|
},
|
||||||
fetchAllyTreaties: async (ally: string) => {
|
fetchAllyTreaties: async (ally: string) => {
|
||||||
let treaties = useMockData ? mockTreaties : (await api.scry<TreatyUpdateIni>(scryAllyTreaties(ally))).ini;
|
let treaties = useMockData
|
||||||
treaties = _.mapValues(treaties, normalizeDocket);
|
? mockTreaties
|
||||||
|
: (await api.scry<TreatyUpdateIni>(scryAllyTreaties(ally))).ini;
|
||||||
|
treaties = mapValues(treaties, normalizeDocket);
|
||||||
set((s) => ({ treaties: { ...s.treaties, ...treaties } }));
|
set((s) => ({ treaties: { ...s.treaties, ...treaties } }));
|
||||||
return treaties;
|
return treaties;
|
||||||
},
|
},
|
||||||
@ -97,10 +91,9 @@ const useDocketState = create<DocketState>((set, get) => ({
|
|||||||
throw new Error('Bad install');
|
throw new Error('Bad install');
|
||||||
}
|
}
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
|
set((state) => addCharge(state, desk, { ...treaty, chad: { install: null } }));
|
||||||
set((state) => addCharge(state, desk, {...treaty, chad: { install: null }}));
|
|
||||||
await new Promise<void>((res) => setTimeout(() => res(), 5000));
|
await new Promise<void>((res) => setTimeout(() => res(), 5000));
|
||||||
set((state) => addCharge(state, desk, {...treaty, chad: { glob: null }}));
|
set((state) => addCharge(state, desk, { ...treaty, chad: { glob: null } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.poke(docketInstall(ship, desk));
|
return api.poke(docketInstall(ship, desk));
|
||||||
@ -117,7 +110,7 @@ const useDocketState = create<DocketState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
toggleDocket: async (desk: string) => {
|
toggleDocket: async (desk: string) => {
|
||||||
if(useMockData) {
|
if (useMockData) {
|
||||||
set(
|
set(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
const charge = draft.charges[desk];
|
const charge = draft.charges[desk];
|
||||||
@ -127,11 +120,11 @@ const useDocketState = create<DocketState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
const { charges } = get();
|
const { charges } = get();
|
||||||
const charge = charges[desk];
|
const charge = charges[desk];
|
||||||
if(!charge) {
|
if (!charge) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const suspended = 'suspend' in charge.chad;
|
const suspended = 'suspend' in charge.chad;
|
||||||
if(suspended) {
|
if (suspended) {
|
||||||
await api.poke(kilnRevive(desk));
|
await api.poke(kilnRevive(desk));
|
||||||
} else {
|
} else {
|
||||||
await api.poke(kilnSuspend(desk));
|
await api.poke(kilnSuspend(desk));
|
||||||
@ -175,15 +168,14 @@ api.subscribe({
|
|||||||
path: '/charges',
|
path: '/charges',
|
||||||
event: (data: ChargeUpdate) => {
|
event: (data: ChargeUpdate) => {
|
||||||
useDocketState.setState((state) => {
|
useDocketState.setState((state) => {
|
||||||
|
|
||||||
if ('add-charge' in data) {
|
if ('add-charge' in data) {
|
||||||
const { desk, charge } = data['add-charge']
|
const { desk, charge } = data['add-charge'];
|
||||||
return addCharge(state, desk, charge)
|
return addCharge(state, desk, charge);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('del-charge' in data) {
|
if ('del-charge' in data) {
|
||||||
const desk = data['del-charge'];
|
const desk = data['del-charge'];
|
||||||
return delCharge(state, desk)
|
return delCharge(state, desk);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { charges: state.charges };
|
return { charges: state.charges };
|
||||||
@ -222,7 +214,7 @@ export function useAllyTreaties(ship: string) {
|
|||||||
useCallback(
|
useCallback(
|
||||||
(s) => {
|
(s) => {
|
||||||
const charter = s.allies[ship];
|
const charter = s.allies[ship];
|
||||||
return _.pick(s.treaties, ...(charter || []));
|
return pick(s.treaties, ...(charter || []));
|
||||||
},
|
},
|
||||||
[ship]
|
[ship]
|
||||||
)
|
)
|
||||||
@ -242,6 +234,6 @@ export function useTreaty(host: string, desk: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// xx useful for debugging
|
// xx useful for debugging
|
||||||
//window.docket = useDocketState.getState;
|
// window.docket = useDocketState.getState;
|
||||||
|
|
||||||
export default useDocketState;
|
export default useDocketState;
|
||||||
|
18
pkg/grid/src/state/hark-types.ts
Normal file
18
pkg/grid/src/state/hark-types.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* I know this doesn't match our current hark type scheme, but since we're talking
|
||||||
|
* about changing that I decided to just throw something together to at least test
|
||||||
|
* this flow for updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface SystemNotification {
|
||||||
|
type: 'system-updates-blocked';
|
||||||
|
charges: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BasicNotification {
|
||||||
|
type: 'basic';
|
||||||
|
time: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Notification = BasicNotification | SystemNotification;
|
12
pkg/grid/src/state/hark.ts
Normal file
12
pkg/grid/src/state/hark.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import create from 'zustand';
|
||||||
|
import { Notification } from './hark-types';
|
||||||
|
import { mockBlockedChargeNotification } from './mock-data';
|
||||||
|
import { useMockData } from './util';
|
||||||
|
|
||||||
|
interface HarkStore {
|
||||||
|
notifications: Notification[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHarkStore = create<HarkStore>(() => ({
|
||||||
|
notifications: useMockData ? [mockBlockedChargeNotification] : []
|
||||||
|
}));
|
@ -1,6 +1,7 @@
|
|||||||
import systemUrl from '../assets/system.png';
|
import _ from 'lodash-es';
|
||||||
import _ from 'lodash';
|
|
||||||
import { Allies, Charges, DocketHrefGlob, Treaties, Treaty } from '@urbit/api/docket';
|
import { Allies, Charges, DocketHrefGlob, Treaties, Treaty } from '@urbit/api/docket';
|
||||||
|
import systemUrl from '../assets/system.png';
|
||||||
|
import { SystemNotification } from './hark-types';
|
||||||
|
|
||||||
export const appMetaData: Pick<Treaty, 'cass' | 'hash' | 'website' | 'license' | 'version'> = {
|
export const appMetaData: Pick<Treaty, 'cass' | 'hash' | 'website' | 'license' | 'version'> = {
|
||||||
cass: '~2021.8.11..05.11.10..b721',
|
cass: '~2021.8.11..05.11.10..b721',
|
||||||
@ -127,9 +128,8 @@ export const mockCharges: Charges = _.reduce(
|
|||||||
mockTreaties,
|
mockTreaties,
|
||||||
(acc, val, key) => {
|
(acc, val, key) => {
|
||||||
const [, desk] = key.split('/');
|
const [, desk] = key.split('/');
|
||||||
const chad = desk === 'uniswap'
|
const chad = { glob: null };
|
||||||
? { install: null } : { glob : null };
|
if (desk === 'inbox') {
|
||||||
if(desk === 'inbox') {
|
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,3 +150,8 @@ export const mockAllies: Allies = [
|
|||||||
'~nalrex_bannus',
|
'~nalrex_bannus',
|
||||||
'~nalrys'
|
'~nalrys'
|
||||||
].reduce((acc, val) => ({ ...acc, [val]: charter }), {});
|
].reduce((acc, val) => ({ ...acc, [val]: charter }), {});
|
||||||
|
|
||||||
|
export const mockBlockedChargeNotification: SystemNotification = {
|
||||||
|
type: 'system-updates-blocked',
|
||||||
|
charges: ['groups', 'pomodoro']
|
||||||
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DocketHref } from "@urbit/api/docket";
|
import { DocketHref } from '@urbit/api/docket';
|
||||||
|
|
||||||
export function makeKeyFn(key: string) {
|
export function makeKeyFn(key: string) {
|
||||||
return (childKeys: string[] = []) => {
|
return (childKeys: string[] = []) => {
|
||||||
@ -16,7 +16,6 @@ export async function fakeRequest<T>(data: T, time = 300): Promise<T> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getAppHref(href: DocketHref) {
|
export function getAppHref(href: DocketHref) {
|
||||||
return 'site' in href ? href.site : `/apps/${href.glob.base}`;
|
return 'site' in href ? href.site : `/apps/${href.glob.base}`;
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,10 @@
|
|||||||
@apply min-w-52 p-4 rounded-xl;
|
@apply min-w-52 p-4 rounded-xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
@apply p-4 bg-gray-100 rounded-xl;
|
||||||
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
@apply inline-flex items-center w-6 h-6 animate-spin;
|
@apply inline-flex items-center w-6 h-6 animate-spin;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
.scroll-full-width {
|
||||||
|
width: calc(100% - var(--removed-body-scroll-bar-size));
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-left-50 {
|
||||||
|
left: calc(50% - (var(--removed-body-scroll-bar-size) / 2));
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import { Button } from '../components/Button';
|
import { Button } from '../components/Button';
|
||||||
import { Dialog, DialogContent } from '../components/Dialog';
|
import { Dialog, DialogClose, DialogContent } from '../components/Dialog';
|
||||||
import useDocketState, { useCharges } from '../state/docket';
|
import useDocketState, { useCharges } from '../state/docket';
|
||||||
|
|
||||||
export const RemoveApp = () => {
|
export const RemoveApp = () => {
|
||||||
@ -14,19 +14,23 @@ export const RemoveApp = () => {
|
|||||||
// TODO: add optimistic updates
|
// TODO: add optimistic updates
|
||||||
const handleRemoveApp = useCallback(() => {
|
const handleRemoveApp = useCallback(() => {
|
||||||
uninstallDocket(desk);
|
uninstallDocket(desk);
|
||||||
history.push('/');
|
}, [desk]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onOpenChange={(open) => !open && history.push('/')}>
|
<Dialog open onOpenChange={(open) => !open && history.push('/')}>
|
||||||
<DialogContent>
|
<DialogContent showClose={false} className="max-w-[400px] space-y-6">
|
||||||
<h1 className="h4 mb-9">Remove “{docket?.title || ''}”</h1>
|
<h1 className="h4">Remove “{docket?.title || ''}”?</h1>
|
||||||
<p className="text-base tracking-tight mb-4 pr-6">
|
<p className="text-base tracking-tight pr-6">
|
||||||
Explanatory writing about what data will be kept.
|
This will remove the software's tile from your home screen.
|
||||||
</p>
|
</p>
|
||||||
<Button variant="destructive" onClick={handleRemoveApp}>
|
<div className="flex space-x-6">
|
||||||
Remove
|
<DialogClose as={Button} variant="secondary">
|
||||||
</Button>
|
Cancel
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose as={Button} onClick={handleRemoveApp}>
|
||||||
|
Remove “{docket?.title}”
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Redirect, useHistory, useParams } from 'react-router-dom';
|
import { Redirect, useHistory, useParams } from 'react-router-dom';
|
||||||
import { Button } from '../components/Button';
|
import { Button } from '../components/Button';
|
||||||
import { Dialog, DialogContent } from '../components/Dialog';
|
import { Dialog, DialogClose, DialogContent } from '../components/Dialog';
|
||||||
import useDocketState, { useCharges } from '../state/docket';
|
import useDocketState, { useCharges } from '../state/docket';
|
||||||
|
|
||||||
export const SuspendApp = () => {
|
export const SuspendApp = () => {
|
||||||
@ -13,7 +13,6 @@ export const SuspendApp = () => {
|
|||||||
// TODO: add optimistic updates
|
// TODO: add optimistic updates
|
||||||
const handleSuspendApp = useCallback(() => {
|
const handleSuspendApp = useCallback(() => {
|
||||||
useDocketState.getState().toggleDocket(desk);
|
useDocketState.getState().toggleDocket(desk);
|
||||||
history.push('/');
|
|
||||||
}, [desk]);
|
}, [desk]);
|
||||||
|
|
||||||
if ('suspend' in charge.chad) {
|
if ('suspend' in charge.chad) {
|
||||||
@ -22,14 +21,19 @@ export const SuspendApp = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onOpenChange={(open) => !open && history.push('/')}>
|
<Dialog open onOpenChange={(open) => !open && history.push('/')}>
|
||||||
<DialogContent>
|
<DialogContent showClose={false} className="max-w-[400px] space-y-6">
|
||||||
<h1 className="h4 mb-9">Suspend “{charge?.title || ''}”</h1>
|
<h1 className="h4">Suspend “{charge?.title || ''}”</h1>
|
||||||
<p className="text-base tracking-tight mb-4 pr-6">
|
<p className="text-base tracking-tight pr-6">
|
||||||
Suspending an app will freeze its current state, and render it unable
|
Suspending an app will freeze its current state, and render it unable
|
||||||
</p>
|
</p>
|
||||||
<Button variant="destructive" onClick={handleSuspendApp}>
|
<div className="flex space-x-6">
|
||||||
Suspend
|
<DialogClose as={Button} variant="secondary">
|
||||||
</Button>
|
Cancel
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose as={Button} onClick={handleSuspendApp}>
|
||||||
|
Suspend “{charge?.title}”
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
@ -5,6 +5,7 @@ import { chadIsRunning, Charge } from '@urbit/api/docket';
|
|||||||
import { TileMenu } from './TileMenu';
|
import { TileMenu } from './TileMenu';
|
||||||
import { Spinner } from '../components/Spinner';
|
import { Spinner } from '../components/Spinner';
|
||||||
import { getAppHref } from '../state/util';
|
import { getAppHref } from '../state/util';
|
||||||
|
import { useRecentsStore } from '../nav/search/Home';
|
||||||
|
|
||||||
type TileProps = {
|
type TileProps = {
|
||||||
charge: Charge;
|
charge: Charge;
|
||||||
@ -24,6 +25,7 @@ function getMenuColor(color: string, lightText: boolean, active: boolean): strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
|
export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
|
||||||
|
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
||||||
const { title, color, image, chad, href } = charge;
|
const { title, color, image, chad, href } = charge;
|
||||||
const loading = 'install' in chad;
|
const loading = 'install' in chad;
|
||||||
const active = chadIsRunning(chad);
|
const active = chadIsRunning(chad);
|
||||||
@ -38,10 +40,12 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
|
|||||||
href={active ? link : undefined}
|
href={active ? link : undefined}
|
||||||
target={desk}
|
target={desk}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-xl default-ring',
|
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring',
|
||||||
!active && 'cursor-default'
|
!active && 'cursor-default'
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: active ? color || 'purple' : suspendColor }}
|
style={{ backgroundColor: active ? color || 'purple' : suspendColor }}
|
||||||
|
onClick={() => addRecentApp(charge)}
|
||||||
|
onAuxClick={() => addRecentApp(charge)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
@ -7,71 +7,61 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
transparent: "transparent",
|
transparent: 'transparent',
|
||||||
white: "#FFFFFF",
|
white: '#FFFFFF',
|
||||||
black: "#000000",
|
black: '#000000',
|
||||||
gray: {
|
gray: {
|
||||||
...colors.trueGray,
|
...colors.trueGray,
|
||||||
100: "#F2F2F2",
|
100: '#F2F2F2',
|
||||||
200: "#CCCCCC",
|
200: '#CCCCCC',
|
||||||
300: "#B3B3B3",
|
300: '#B3B3B3',
|
||||||
400: "#808080",
|
400: '#808080',
|
||||||
500: "#666666",
|
500: '#666666'
|
||||||
},
|
},
|
||||||
blue: {
|
blue: {
|
||||||
100: "#E9F5FF",
|
100: '#E9F5FF',
|
||||||
200: "#D3EBFF",
|
200: '#D3EBFF',
|
||||||
300: "#BCE2FF",
|
300: '#BCE2FF',
|
||||||
400: "#219DFF",
|
400: '#219DFF'
|
||||||
},
|
},
|
||||||
red: {
|
red: {
|
||||||
100: "#FFF6F5",
|
100: '#FFF6F5',
|
||||||
200: "#FFC6C3",
|
200: '#FFC6C3',
|
||||||
400: "#FF4136",
|
400: '#FF4136'
|
||||||
},
|
},
|
||||||
green: {
|
green: {
|
||||||
100: "#E6F5F0",
|
100: '#E6F5F0',
|
||||||
200: "#B3E2D1",
|
200: '#B3E2D1',
|
||||||
400: "#009F65",
|
400: '#009F65'
|
||||||
},
|
},
|
||||||
yellow: {
|
yellow: {
|
||||||
100: "#FFF9E6",
|
100: '#FFF9E6',
|
||||||
200: "#FFEEB3",
|
200: '#FFEEB3',
|
||||||
300: "#FFDD66",
|
300: '#FFDD66',
|
||||||
400: "#FFC700",
|
400: '#FFC700'
|
||||||
},
|
},
|
||||||
orange: {
|
orange: colors.orange
|
||||||
100: '#FFF2EB'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: [
|
sans: [
|
||||||
'"Inter"',
|
'"Inter"',
|
||||||
'"Inter UI"',
|
'"Inter UI"',
|
||||||
"-apple-system",
|
'-apple-system',
|
||||||
"BlinkMacSystemFont",
|
'BlinkMacSystemFont',
|
||||||
'"San Francisco"',
|
'"San Francisco"',
|
||||||
'"Helvetica Neue"',
|
'"Helvetica Neue"',
|
||||||
"Arial",
|
'Arial',
|
||||||
"sans-serif",
|
'sans-serif'
|
||||||
],
|
|
||||||
mono: [
|
|
||||||
'"Source Code Pro"',
|
|
||||||
'"Roboto mono"',
|
|
||||||
'"Courier New"',
|
|
||||||
"monospace",
|
|
||||||
],
|
],
|
||||||
|
mono: ['"Source Code Pro"', '"Roboto mono"', '"Courier New"', 'monospace']
|
||||||
},
|
},
|
||||||
minWidth: theme => theme('spacing'),
|
minWidth: (theme) => theme('spacing')
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
extend: {
|
extend: {
|
||||||
opacity: ['hover-none']
|
opacity: ['hover-none']
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [require('@tailwindcss/aspect-ratio'), require('tailwindcss-touch')()]
|
||||||
require('@tailwindcss/aspect-ratio'),
|
};
|
||||||
require('tailwindcss-touch')()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user