mirror of
https://github.com/urbit/shrub.git
synced 2024-12-26 13:31:36 +03:00
Merge remote-tracking branch 'origin/dist' into lf/nu-hark-store
This commit is contained in:
commit
94fc096b80
16
.vercelignore
Normal file
16
.vercelignore
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
bin
|
||||||
|
doc
|
||||||
|
extras
|
||||||
|
nix
|
||||||
|
pkg/arvo
|
||||||
|
pkg/base-dev
|
||||||
|
pkg/docker-image
|
||||||
|
pkg/ent
|
||||||
|
pkg/garden
|
||||||
|
pkg/garden-dev
|
||||||
|
pkg/ge-additions
|
||||||
|
pkg/herb
|
||||||
|
pkg/hs
|
||||||
|
pkg/libaes_siv
|
||||||
|
pkg/urbit
|
||||||
|
sh
|
8839
package-lock.json
generated
8839
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13693
pkg/btc-wallet/package-lock.json
generated
13693
pkg/btc-wallet/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,8 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
<link rel="icon" href="/src/assets/favicon.png" />
|
||||||
|
<link rel="icon" href="/src/assets/favicon.svg" sizes="any" type="image/svg+xml" />
|
||||||
<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" />
|
||||||
|
12233
pkg/grid/package-lock.json
generated
12233
pkg/grid/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
import { BrowserRouter, Switch, Route, useHistory, useLocation } from 'react-router-dom';
|
import { BrowserRouter, Switch, Route, useHistory, useLocation } from 'react-router-dom';
|
||||||
import { Grid } from './pages/Grid';
|
import { Grid } from './pages/Grid';
|
||||||
@ -7,6 +7,7 @@ import { PermalinkRoutes } from './pages/PermalinkRoutes';
|
|||||||
import useKilnState from './state/kiln';
|
import useKilnState from './state/kiln';
|
||||||
import useContactState from './state/contact';
|
import useContactState from './state/contact';
|
||||||
import api from './state/api';
|
import api from './state/api';
|
||||||
|
import { useMedia } from './logic/useMedia';
|
||||||
import { useHarkStore } from './state/hark';
|
import { useHarkStore } from './state/hark';
|
||||||
import { useTheme } from './state/settings';
|
import { useTheme } from './state/settings';
|
||||||
import { useLocalState } from './state/local';
|
import { useLocalState } from './state/local';
|
||||||
@ -24,36 +25,24 @@ const AppRoutes = () => {
|
|||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const query = new URLSearchParams(location.search);
|
const query = new URLSearchParams(search);
|
||||||
if (query.has('grid-note')) {
|
if (query.has('grid-note')) {
|
||||||
const redir = getNoteRedirect(query.get('grid-note')!);
|
const redir = getNoteRedirect(query.get('grid-note')!);
|
||||||
push(redir);
|
push(redir);
|
||||||
}
|
}
|
||||||
}, [location.search]);
|
}, [search]);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const isDarkMode = useMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
const updateThemeClass = useCallback(
|
useEffect(() => {
|
||||||
(e: MediaQueryListEvent) => {
|
if ((isDarkMode && theme === 'auto') || theme === 'dark') {
|
||||||
if ((e.matches && theme === 'auto') || theme === 'dark') {
|
|
||||||
document.body.classList.add('dark');
|
document.body.classList.add('dark');
|
||||||
useLocalState.setState({ currentTheme: 'dark' });
|
useLocalState.setState({ currentTheme: 'dark' });
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.remove('dark');
|
document.body.classList.remove('dark');
|
||||||
useLocalState.setState({ currentTheme: 'light' });
|
useLocalState.setState({ currentTheme: 'light' });
|
||||||
}
|
}
|
||||||
},
|
}, [isDarkMode, theme]);
|
||||||
[theme]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const query = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
|
|
||||||
query.addEventListener('change', updateThemeClass);
|
|
||||||
updateThemeClass({ matches: query.matches } as MediaQueryListEvent);
|
|
||||||
return () => {
|
|
||||||
query.removeEventListener('change', updateThemeClass);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {}, []);
|
useEffect(() => {}, []);
|
||||||
|
|
||||||
|
BIN
pkg/grid/src/assets/favicon.png
Normal file
BIN
pkg/grid/src/assets/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
11
pkg/grid/src/assets/favicon.svg
Normal file
11
pkg/grid/src/assets/favicon.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
rect { fill: #ffffff; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<rect x="51" y="51" width="173" height="173" rx="24" fill="#444444"/>
|
||||||
|
<rect x="51" y="288" width="173" height="173" rx="24" fill="#444444"/>
|
||||||
|
<rect x="288" y="51" width="173" height="173" rx="24" fill="#444444"/>
|
||||||
|
<rect x="288" y="288" width="173" height="173" rx="24" fill="#444444"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 474 B |
@ -2,6 +2,8 @@ import classNames from 'classnames';
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { sigil, reactRenderer } from '@tlon/sigil-js';
|
import { sigil, reactRenderer } from '@tlon/sigil-js';
|
||||||
import { deSig, Contact } from '@urbit/api';
|
import { deSig, Contact } from '@urbit/api';
|
||||||
|
import { darken, lighten, parseToHsla } from 'color2k';
|
||||||
|
import { useCurrentTheme } from '../state/local';
|
||||||
|
|
||||||
export type AvatarSizes = 'xs' | 'small' | 'default';
|
export type AvatarSizes = 'xs' | 'small' | 'default';
|
||||||
|
|
||||||
@ -45,10 +47,27 @@ const emptyContact: Contact = {
|
|||||||
'last-updated': 0
|
'last-updated': 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function themeAdjustColor(color: string, theme: 'light' | 'dark'): string {
|
||||||
|
const hsla = parseToHsla(color);
|
||||||
|
const lightness = hsla[2];
|
||||||
|
|
||||||
|
if (lightness <= 0.1 && theme === 'dark') {
|
||||||
|
return lighten(color, 0.1 - lightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lightness >= 0.9 && theme === 'light') {
|
||||||
|
return darken(color, lightness - 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
export const Avatar = ({ size, className, ...ship }: AvatarProps) => {
|
export const Avatar = ({ size, className, ...ship }: AvatarProps) => {
|
||||||
|
const currentTheme = useCurrentTheme();
|
||||||
const { shipName, color, avatar } = { ...emptyContact, ...ship };
|
const { shipName, color, avatar } = { ...emptyContact, ...ship };
|
||||||
const { classes, size: sigilSize } = sizeMap[size];
|
const { classes, size: sigilSize } = sizeMap[size];
|
||||||
const foregroundColor = foregroundFromBackground(color);
|
const adjustedColor = themeAdjustColor(color, currentTheme);
|
||||||
|
const foregroundColor = foregroundFromBackground(adjustedColor);
|
||||||
const sigilElement = useMemo(() => {
|
const sigilElement = useMemo(() => {
|
||||||
if (shipName.match(/[_^]/)) {
|
if (shipName.match(/[_^]/)) {
|
||||||
return null;
|
return null;
|
||||||
@ -59,9 +78,9 @@ export const Avatar = ({ size, className, ...ship }: AvatarProps) => {
|
|||||||
renderer: reactRenderer,
|
renderer: reactRenderer,
|
||||||
size: sigilSize,
|
size: sigilSize,
|
||||||
icon: true,
|
icon: true,
|
||||||
colors: [color, foregroundColor]
|
colors: [adjustedColor, foregroundColor]
|
||||||
});
|
});
|
||||||
}, [shipName, color, foregroundColor]);
|
}, [shipName, adjustedColor, foregroundColor]);
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
return <img className={classNames('', classes)} src={avatar} alt="" />;
|
return <img className={classNames('', classes)} src={avatar} alt="" />;
|
||||||
@ -77,7 +96,7 @@ export const Avatar = ({ size, className, ...ship }: AvatarProps) => {
|
|||||||
size === 'default' && 'p-3',
|
size === 'default' && 'p-3',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: color }}
|
style={{ backgroundColor: adjustedColor }}
|
||||||
>
|
>
|
||||||
{sigilElement}
|
{sigilElement}
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,7 +22,7 @@ const variants: Record<ButtonVariant, string> = {
|
|||||||
secondary: 'text-black bg-gray-100',
|
secondary: 'text-black bg-gray-100',
|
||||||
caution: 'text-white bg-orange-400',
|
caution: 'text-white bg-orange-400',
|
||||||
destructive: 'text-white bg-red-500',
|
destructive: 'text-white bg-red-500',
|
||||||
'alt-primary': 'text-white bg-blue-400',
|
'alt-primary': 'text-white bg-blue-400 ring-blue-300',
|
||||||
'alt-secondary': 'text-blue-400 bg-blue-50'
|
'alt-secondary': 'text-blue-400 bg-blue-50'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 1.5 KiB |
21
pkg/grid/src/logic/useMedia.ts
Normal file
21
pkg/grid/src/logic/useMedia.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const useMedia = (mediaQuery: string) => {
|
||||||
|
const [match, setMatch] = useState(false);
|
||||||
|
|
||||||
|
const update = useCallback((e: MediaQueryListEvent) => {
|
||||||
|
setMatch(e.matches);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const query = window.matchMedia(mediaQuery);
|
||||||
|
|
||||||
|
query.addEventListener('change', update);
|
||||||
|
update({ matches: query.matches } as MediaQueryListEvent);
|
||||||
|
return () => {
|
||||||
|
query.removeEventListener('change', update);
|
||||||
|
};
|
||||||
|
}, [update]);
|
||||||
|
|
||||||
|
return match;
|
||||||
|
};
|
@ -35,9 +35,21 @@ type LeapProps = {
|
|||||||
menu: MenuState;
|
menu: MenuState;
|
||||||
dropdown: string;
|
dropdown: string;
|
||||||
navOpen: boolean;
|
navOpen: boolean;
|
||||||
|
shouldDim: boolean;
|
||||||
} & HTMLAttributes<HTMLDivElement>;
|
} & HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const Leap = React.forwardRef(({ menu, dropdown, navOpen, className }: LeapProps, ref) => {
|
function normalizeMatchString(match: string, keepAltChars: boolean): string {
|
||||||
|
let normalizedString = match.toLocaleLowerCase().trim();
|
||||||
|
|
||||||
|
if (!keepAltChars) {
|
||||||
|
normalizedString = normalizedString.replace(/[^\w]/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Leap = React.forwardRef(
|
||||||
|
({ menu, dropdown, navOpen, shouldDim, className }: LeapProps, ref) => {
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const match = useRouteMatch<{ menu?: MenuState; query?: string; desk?: string }>(
|
const match = useRouteMatch<{ menu?: MenuState; query?: string; desk?: string }>(
|
||||||
`/leap/${menu}/:query?/(apps)?/:desk?`
|
`/leap/${menu}/:query?/(apps)?/:desk?`
|
||||||
@ -76,11 +88,12 @@ export const Leap = React.forwardRef(({ menu, dropdown, navOpen, className }: Le
|
|||||||
|
|
||||||
const getMatch = useCallback(
|
const getMatch = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
const normValue = value.toLocaleLowerCase();
|
const onlySymbols = !value.match(/[\w]/g);
|
||||||
|
const normValue = normalizeMatchString(value, onlySymbols);
|
||||||
return matches.find(
|
return matches.find(
|
||||||
(m) =>
|
(m) =>
|
||||||
m.display?.toLocaleLowerCase().startsWith(normValue) ||
|
(m.display && normalizeMatchString(m.display, onlySymbols).startsWith(normValue)) ||
|
||||||
m.value.toLocaleLowerCase().startsWith(normValue)
|
normalizeMatchString(m.value, onlySymbols).startsWith(normValue)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[matches]
|
[matches]
|
||||||
@ -119,7 +132,10 @@ export const Leap = React.forwardRef(({ menu, dropdown, navOpen, className }: Le
|
|||||||
|
|
||||||
if (matchValue && inputRef.current && !isDeletion) {
|
if (matchValue && inputRef.current && !isDeletion) {
|
||||||
inputRef.current.value = matchValue;
|
inputRef.current.value = matchValue;
|
||||||
inputRef.current.setSelectionRange(value.length, matchValue.length);
|
const start = matchValue.startsWith(value)
|
||||||
|
? value.length
|
||||||
|
: matchValue.substring(0, matchValue.indexOf(value)).length + value.length;
|
||||||
|
inputRef.current.setSelectionRange(start, matchValue.length);
|
||||||
useLeapStore.setState({
|
useLeapStore.setState({
|
||||||
rawInput: matchValue,
|
rawInput: matchValue,
|
||||||
selectedMatch: inputMatch
|
selectedMatch: inputMatch
|
||||||
@ -200,7 +216,7 @@ export const Leap = React.forwardRef(({ menu, dropdown, navOpen, className }: Le
|
|||||||
<form
|
<form
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex items-center h-full w-full px-2 rounded-full bg-white default-ring focus-within:ring-2',
|
'flex items-center h-full w-full px-2 rounded-full bg-white default-ring focus-within:ring-2',
|
||||||
navOpen && menu !== 'search' && 'opacity-60',
|
shouldDim && 'opacity-60',
|
||||||
!navOpen ? 'bg-gray-50' : '',
|
!navOpen ? 'bg-gray-50' : '',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@ -220,7 +236,7 @@ export const Leap = React.forwardRef(({ menu, dropdown, navOpen, className }: Le
|
|||||||
type="text"
|
type="text"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={selection ? '' : 'Search Landscape'}
|
placeholder={selection ? '' : 'Search Landscape'}
|
||||||
className="flex-1 w-full h-full px-2 h4 rounded-full bg-transparent outline-none"
|
className="flex-1 w-full h-full px-2 h4 text-base rounded-full bg-transparent outline-none"
|
||||||
value={rawInput}
|
value={rawInput}
|
||||||
onClick={toggleSearch}
|
onClick={toggleSearch}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
@ -231,7 +247,7 @@ export const Leap = React.forwardRef(({ menu, dropdown, navOpen, className }: Le
|
|||||||
aria-activedescendant={selectedMatch?.display || selectedMatch?.value}
|
aria-activedescendant={selectedMatch?.display || selectedMatch?.value}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
{navOpen && (
|
{menu === 'search' && (
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="absolute top-1/2 right-2 flex-none circle-button w-8 h-8 text-gray-400 bg-gray-50 default-ring -translate-y-1/2"
|
className="absolute top-1/2 right-2 flex-none circle-button w-8 h-8 text-gray-400 bg-gray-50 default-ring -translate-y-1/2"
|
||||||
@ -243,4 +259,5 @@ export const Leap = React.forwardRef(({ menu, dropdown, navOpen, className }: Le
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
@ -52,33 +52,6 @@ export type MenuState =
|
|||||||
| 'help-and-support'
|
| 'help-and-support'
|
||||||
| 'system-preferences';
|
| 'system-preferences';
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
if (parts[parts.length - 2] === 'apps') {
|
|
||||||
parts.pop();
|
|
||||||
}
|
|
||||||
return parts.join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NavProps {
|
interface NavProps {
|
||||||
menu?: MenuState;
|
menu?: MenuState;
|
||||||
}
|
}
|
||||||
@ -123,6 +96,15 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const preventClose = useCallback((e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const hasNavAncestor = target.closest('#dialog-nav');
|
||||||
|
|
||||||
|
if (hasNavAncestor) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Using portal so that we can retain the same nav items both in the dialog and in the base header */}
|
{/* Using portal so that we can retain the same nav items both in the dialog and in the base header */}
|
||||||
@ -132,12 +114,22 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
|||||||
>
|
>
|
||||||
<SystemMenu
|
<SystemMenu
|
||||||
open={!!systemMenuOpen}
|
open={!!systemMenuOpen}
|
||||||
menu={menuState}
|
subMenuOpen={menu === 'system-preferences' || menu === 'help-and-support'}
|
||||||
navOpen={isOpen}
|
shouldDim={isOpen && menu !== 'system-preferences' && menu !== 'help-and-support'}
|
||||||
className={classNames('relative z-50 flex-none', eitherOpen ? 'bg-white' : 'bg-gray-50')}
|
className={classNames('relative z-50 flex-none', eitherOpen ? 'bg-white' : 'bg-gray-50')}
|
||||||
/>
|
/>
|
||||||
<NotificationsLink menu={menuState} navOpen={isOpen} />
|
<NotificationsLink
|
||||||
<Leap ref={inputRef} menu={menuState} dropdown="leap-items" navOpen={isOpen} />
|
navOpen={isOpen}
|
||||||
|
notificationsOpen={menu === 'notifications'}
|
||||||
|
shouldDim={(isOpen && menu !== 'notifications') || !!systemMenuOpen}
|
||||||
|
/>
|
||||||
|
<Leap
|
||||||
|
ref={inputRef}
|
||||||
|
menu={menuState}
|
||||||
|
dropdown="leap-items"
|
||||||
|
navOpen={isOpen}
|
||||||
|
shouldDim={(isOpen && menu !== 'search') || !!systemMenuOpen}
|
||||||
|
/>
|
||||||
</Portal.Root>
|
</Portal.Root>
|
||||||
<div
|
<div
|
||||||
ref={navRef}
|
ref={navRef}
|
||||||
@ -152,6 +144,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
|||||||
/>
|
/>
|
||||||
<Dialog open={isOpen} onOpenChange={onDialogClose}>
|
<Dialog open={isOpen} onOpenChange={onDialogClose}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
|
onInteractOutside={preventClose}
|
||||||
onOpenAutoFocus={onOpen}
|
onOpenAutoFocus={onOpen}
|
||||||
className="fixed bottom-0 sm:top-0 sm:bottom-auto scroll-left-50 flex flex-col scroll-full-width max-w-[882px] px-4 sm:pb-4 text-gray-400 -translate-x-1/2 outline-none"
|
className="fixed bottom-0 sm:top-0 sm:bottom-auto scroll-left-50 flex flex-col scroll-full-width max-w-[882px] px-4 sm:pb-4 text-gray-400 -translate-x-1/2 outline-none"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
@ -160,8 +153,9 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
|||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
|
id="dialog-nav"
|
||||||
ref={dialogNavRef}
|
ref={dialogNavRef}
|
||||||
className="max-w-[712px] w-full mx-auto mt-6 mb-3 order-last sm:order-none"
|
className="max-w-[712px] w-full mx-auto my-6 sm:mb-3 order-last sm:order-none"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
id="leap-items"
|
id="leap-items"
|
||||||
|
@ -1,18 +1,25 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Notification, Timebox } from '@urbit/api';
|
import { Timebox } from '@urbit/api';
|
||||||
import { Link, LinkProps } from 'react-router-dom';
|
import { Link, LinkProps } from 'react-router-dom';
|
||||||
import { Bullet } from '../components/icons/Bullet';
|
import { Bullet } from '../components/icons/Bullet';
|
||||||
import { useNotifications } from '../state/notifications';
|
import { Cross } from '../components/icons/Cross';
|
||||||
import { MenuState } from './Nav';
|
import { useHarkStore } from '../state/hark';
|
||||||
|
|
||||||
type NotificationsState = 'empty' | 'unread' | 'attention-needed';
|
type NotificationsState = 'empty' | 'unread' | 'attention-needed' | 'open';
|
||||||
|
|
||||||
function getNotificationsState(box: Timebox): NotificationsState {
|
function getNotificationsState(isOpen: boolean, box: Timebox): NotificationsState {
|
||||||
const notifications = Object.values(box);
|
const notifications = Object.values(box);
|
||||||
if (notifications.filter(({ bin }) => bin.place.desk === window.desk).length > 0) {
|
if (
|
||||||
|
notifications.filter(
|
||||||
|
({ bin }) => bin.place.desk === window.desk && ['/lag', 'blocked'].includes(bin.place.path)
|
||||||
|
).length > 0
|
||||||
|
) {
|
||||||
return 'attention-needed';
|
return 'attention-needed';
|
||||||
}
|
}
|
||||||
|
if (isOpen) {
|
||||||
|
return 'open';
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: when real structure, this should be actually be unread not just existence
|
// TODO: when real structure, this should be actually be unread not just existence
|
||||||
if (notifications.length > 0) {
|
if (notifications.length > 0) {
|
||||||
@ -23,21 +30,27 @@ function getNotificationsState(box: Timebox): NotificationsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NotificationsLinkProps = Omit<LinkProps<HTMLAnchorElement>, 'to'> & {
|
type NotificationsLinkProps = Omit<LinkProps<HTMLAnchorElement>, 'to'> & {
|
||||||
menu: MenuState;
|
|
||||||
navOpen: boolean;
|
navOpen: boolean;
|
||||||
|
notificationsOpen: boolean;
|
||||||
|
shouldDim: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotificationsLink = ({ navOpen, menu }: NotificationsLinkProps) => {
|
export const NotificationsLink = ({
|
||||||
const { unseen } = useNotifications();
|
navOpen,
|
||||||
const state = getNotificationsState(unseen);
|
notificationsOpen,
|
||||||
|
shouldDim
|
||||||
|
}: NotificationsLinkProps) => {
|
||||||
|
const unseen = useHarkStore((s) => s.unseen);
|
||||||
|
const state = getNotificationsState(notificationsOpen, unseen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to="/leap/notifications"
|
to={state === 'open' ? '/' : '/leap/notifications'}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'relative z-50 flex-none circle-button h4 default-ring',
|
'relative z-50 flex-none circle-button h4 default-ring',
|
||||||
navOpen && 'text-opacity-60',
|
navOpen && 'text-opacity-60',
|
||||||
navOpen && menu !== 'notifications' && 'opacity-60',
|
shouldDim && 'opacity-60',
|
||||||
|
state === 'open' && 'text-gray-400 bg-white',
|
||||||
state === 'empty' && !navOpen && 'text-gray-400 bg-gray-50',
|
state === 'empty' && !navOpen && 'text-gray-400 bg-gray-50',
|
||||||
state === 'empty' && navOpen && 'text-gray-400 bg-white',
|
state === 'empty' && navOpen && 'text-gray-400 bg-white',
|
||||||
state === 'unread' && 'bg-blue-400 text-white',
|
state === 'unread' && 'bg-blue-400 text-white',
|
||||||
@ -51,6 +64,12 @@ export const NotificationsLink = ({ navOpen, menu }: NotificationsLinkProps) =>
|
|||||||
! <span className="sr-only">Attention needed</span>
|
! <span className="sr-only">Attention needed</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{state === 'open' && (
|
||||||
|
<>
|
||||||
|
<Cross className="w-3 h-3 fill-current" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,12 +7,13 @@ import { Vat } from '@urbit/api/hood';
|
|||||||
import { Adjust } from '../components/icons/Adjust';
|
import { Adjust } from '../components/icons/Adjust';
|
||||||
import { useVat } from '../state/kiln';
|
import { useVat } from '../state/kiln';
|
||||||
import { disableDefault, handleDropdownLink } from '../state/util';
|
import { disableDefault, handleDropdownLink } from '../state/util';
|
||||||
import { MenuState } from './Nav';
|
import { useMedia } from '../logic/useMedia';
|
||||||
|
import { Cross } from '../components/icons/Cross';
|
||||||
|
|
||||||
type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & {
|
type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & {
|
||||||
menu: MenuState;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
navOpen: boolean;
|
subMenuOpen: boolean;
|
||||||
|
shouldDim: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getHash(vat: Vat): string {
|
function getHash(vat: Vat): string {
|
||||||
@ -20,11 +21,12 @@ function getHash(vat: Vat): string {
|
|||||||
return parts[parts.length - 1];
|
return parts[parts.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SystemMenu = ({ className, menu, open, navOpen }: SystemMenuProps) => {
|
export const SystemMenu = ({ className, open, subMenuOpen, shouldDim }: SystemMenuProps) => {
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const garden = useVat('garden');
|
const garden = useVat('garden');
|
||||||
const hash = garden ? getHash(garden) : null;
|
const hash = garden ? getHash(garden) : null;
|
||||||
|
const isMobile = useMedia('(max-width: 639px)');
|
||||||
|
|
||||||
const copyHash = useCallback((event: Event) => {
|
const copyHash = useCallback((event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -53,29 +55,37 @@ export const SystemMenu = ({ className, menu, open, navOpen }: SystemMenuProps)
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(isOpen) => setTimeout(() => !isOpen && push('/'), 15)}
|
onOpenChange={(isOpen) => setTimeout(() => !isOpen && push('/'), 15)}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger
|
<Link
|
||||||
as={Link}
|
to={open || subMenuOpen ? '/' : '/system-menu'}
|
||||||
to="/system-menu"
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'appearance-none circle-button default-ring',
|
'relative appearance-none circle-button default-ring',
|
||||||
open && 'text-gray-300',
|
open && 'text-gray-300',
|
||||||
navOpen &&
|
shouldDim && 'opacity-60',
|
||||||
menu !== 'system-preferences' &&
|
|
||||||
menu !== 'help-and-support' &&
|
|
||||||
'opacity-60',
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{!open && !subMenuOpen && (
|
||||||
|
<>
|
||||||
<Adjust className="w-6 h-6 fill-current text-gray" />
|
<Adjust className="w-6 h-6 fill-current text-gray" />
|
||||||
<span className="sr-only">System Menu</span>
|
<span className="sr-only">System Menu</span>
|
||||||
</DropdownMenu.Trigger>
|
</>
|
||||||
|
)}
|
||||||
|
{(open || subMenuOpen) && (
|
||||||
|
<>
|
||||||
|
<Cross className="w-3 h-3 fill-current" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* trigger here just for anchoring the dropdown */}
|
||||||
|
<DropdownMenu.Trigger className="sr-only top-0 left-0 sm:top-auto sm:left-auto sm:bottom-0" />
|
||||||
|
</Link>
|
||||||
<Route path="/system-menu">
|
<Route path="/system-menu">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
portalled={false}
|
|
||||||
onCloseAutoFocus={disableDefault}
|
onCloseAutoFocus={disableDefault}
|
||||||
onInteractOutside={preventFlash}
|
onInteractOutside={preventFlash}
|
||||||
onFocusOutside={preventFlash}
|
onFocusOutside={preventFlash}
|
||||||
onPointerDownOutside={preventFlash}
|
onPointerDownOutside={preventFlash}
|
||||||
|
side={isMobile ? 'top' : 'bottom'}
|
||||||
sideOffset={12}
|
sideOffset={12}
|
||||||
className="dropdown relative z-40 min-w-64 p-4 font-semibold text-gray-500 bg-white"
|
className="dropdown relative z-40 min-w-64 p-4 font-semibold text-gray-500 bg-white"
|
||||||
>
|
>
|
||||||
|
@ -60,8 +60,8 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
|
|||||||
const subUrl = useCallback((submenu: string) => `${match.url}/${submenu}`, [match]);
|
const subUrl = useCallback((submenu: string) => `${match.url}/${submenu}`, [match]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[600px] max-h-full">
|
<div className="flex h-full overflow-y-auto">
|
||||||
<aside className="flex-none min-w-60 py-8 font-semibold border-r-2 border-gray-50">
|
<aside className="flex-none self-start min-w-60 py-8 font-semibold border-r-2 border-gray-50">
|
||||||
<nav className="px-6">
|
<nav className="px-6">
|
||||||
<ul>
|
<ul>
|
||||||
<SystemPreferencesSection
|
<SystemPreferencesSection
|
||||||
@ -100,7 +100,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<section className="flex-1 p-8 text-black">
|
<section className="flex-1 min-h-[600px] p-8 text-black">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${match.url}/apps/:desk`} component={AppPrefs} />
|
<Route path={`${match.url}/apps/:desk`} component={AppPrefs} />
|
||||||
<Route path={`${match.url}/system-updates`} component={SystemUpdatePrefs} />
|
<Route path={`${match.url}/system-updates`} component={SystemUpdatePrefs} />
|
||||||
|
@ -6,6 +6,7 @@ import { ShipName } from '../../components/ShipName';
|
|||||||
import useDocketState, { useAllyTreaties } from '../../state/docket';
|
import useDocketState, { useAllyTreaties } from '../../state/docket';
|
||||||
import { useLeapStore } from '../Nav';
|
import { useLeapStore } from '../Nav';
|
||||||
import { AppList } from '../../components/AppList';
|
import { AppList } from '../../components/AppList';
|
||||||
|
import { addRecentDev } from './Home';
|
||||||
|
|
||||||
type AppsProps = RouteComponentProps<{ ship: string }>;
|
type AppsProps = RouteComponentProps<{ ship: string }>;
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ export const Apps = ({ match }: AppsProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (provider) {
|
if (provider) {
|
||||||
useDocketState.getState().fetchAllyTreaties(provider);
|
useDocketState.getState().fetchAllyTreaties(provider);
|
||||||
|
addRecentDev(provider);
|
||||||
}
|
}
|
||||||
}, [provider]);
|
}, [provider]);
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ export const Home = () => {
|
|||||||
Recent Apps
|
Recent Apps
|
||||||
</h2>
|
</h2>
|
||||||
{recentApps.length === 0 && (
|
{recentApps.length === 0 && (
|
||||||
<div className="min-h-[150px] p-6 rounded-xl bg-gray-100">
|
<div className="min-h-[150px] p-6 rounded-xl bg-gray-50">
|
||||||
<p className="mb-4">Apps you use will be listed here, in the order you used them.</p>
|
<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>
|
<p className="mb-6">You can click/tap/keyboard on a listed app to open it.</p>
|
||||||
{groups && (
|
{groups && (
|
||||||
@ -122,7 +122,7 @@ export const Home = () => {
|
|||||||
Recent Developers
|
Recent Developers
|
||||||
</h2>
|
</h2>
|
||||||
{recentDevs.length === 0 && (
|
{recentDevs.length === 0 && (
|
||||||
<div className="min-h-[150px] p-6 rounded-xl bg-gray-100">
|
<div className="min-h-[150px] p-6 rounded-xl bg-gray-50">
|
||||||
<p className="mb-4">Urbit app developers you search for will be listed here.</p>
|
<p className="mb-4">Urbit app developers you search for will be listed here.</p>
|
||||||
{zod && (
|
{zod && (
|
||||||
<>
|
<>
|
||||||
|
@ -25,7 +25,7 @@ export const Grid: FunctionComponent<GridProps> = ({ match }) => {
|
|||||||
<main className="h-full w-full flex justify-center pt-4 md:pt-16 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-4 px-4 md:px-8 w-full max-w-6xl">
|
<div className="grid justify-center grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(auto,250px))] gap-4 px-4 md:px-8 w-full max-w-6xl">
|
||||||
{charges &&
|
{charges &&
|
||||||
map(omit(charges, window.desk), (charge, desk) => (
|
map(omit(charges, window.desk), (charge, desk) => (
|
||||||
<Tile key={desk} charge={charge} desk={desk} />
|
<Tile key={desk} charge={charge} desk={desk} />
|
||||||
|
@ -287,7 +287,8 @@ export const mockContacts: Contacts = {
|
|||||||
},
|
},
|
||||||
'~nalbel_litzod': {
|
'~nalbel_litzod': {
|
||||||
...contact,
|
...contact,
|
||||||
nickname: 'Queen'
|
nickname: 'Queen',
|
||||||
|
color: '#0a1b0a'
|
||||||
},
|
},
|
||||||
'~litmus^ritten': {
|
'~litmus^ritten': {
|
||||||
...contact
|
...contact
|
||||||
@ -301,7 +302,8 @@ export const mockContacts: Contacts = {
|
|||||||
},
|
},
|
||||||
'~nalrys': {
|
'~nalrys': {
|
||||||
...contact,
|
...contact,
|
||||||
status: 'hosting coming soon'
|
status: 'hosting coming soon',
|
||||||
|
color: '#130c06'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
@apply px-4 py-2 w-full bg-white rounded-xl;
|
@apply px-4 py-2 w-full text-base sm:text-sm bg-white rounded-xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
|
@ -24,7 +24,7 @@ export const SuspendApp = () => {
|
|||||||
<DialogContent showClose={false} className="max-w-[400px] space-y-6">
|
<DialogContent showClose={false} className="max-w-[400px] space-y-6">
|
||||||
<h1 className="h4">Suspend “{charge?.title || ''}”</h1>
|
<h1 className="h4">Suspend “{charge?.title || ''}”</h1>
|
||||||
<p className="text-base tracking-tight 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>
|
||||||
<div className="flex space-x-6">
|
<div className="flex space-x-6">
|
||||||
<DialogClose as={Button} variant="secondary">
|
<DialogClose as={Button} variant="secondary">
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
import { darken, hsla, lighten, parseToHsla, readableColorIsBlack } from 'color2k';
|
|
||||||
import { chadIsRunning } from '@urbit/api';
|
import { chadIsRunning } from '@urbit/api';
|
||||||
import { TileMenu } from './TileMenu';
|
import { TileMenu } from './TileMenu';
|
||||||
import { Spinner } from '../components/Spinner';
|
import { Spinner } from '../components/Spinner';
|
||||||
@ -14,28 +13,13 @@ type TileProps = {
|
|||||||
desk: string;
|
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> = ({ charge, desk }) => {
|
export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
|
||||||
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
||||||
const { title, image, color, chad, href } = charge;
|
const { title, image, color, chad, href } = charge;
|
||||||
const { theme, tileColor } = useTileColor(color);
|
const { lightText, tileColor, menuColor, suspendColor, suspendMenuColor } = useTileColor(color);
|
||||||
const loading = 'install' in chad;
|
const loading = 'install' in chad;
|
||||||
const active = chadIsRunning(chad);
|
|
||||||
const lightText = !readableColorIsBlack(color);
|
|
||||||
const menuColor = getMenuColor(tileColor, theme === 'dark' ? !lightText : lightText, active);
|
|
||||||
const suspendColor = 'rgb(220,220,220)';
|
|
||||||
const suspended = 'suspend' in chad;
|
const suspended = 'suspend' in chad;
|
||||||
|
const active = chadIsRunning(chad);
|
||||||
const link = getAppHref(href);
|
const link = getAppHref(href);
|
||||||
const backgroundColor = active ? tileColor || 'purple' : suspendColor;
|
const backgroundColor = active ? tileColor || 'purple' : suspendColor;
|
||||||
|
|
||||||
@ -54,20 +38,20 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="absolute z-10 top-4 left-4 lg:top-8 lg:left-8 flex items-center justify-center">
|
<div className="absolute z-10 top-4 left-4 sm:top-8 sm:left-8 flex items-center justify-center">
|
||||||
<Spinner className="h-6 w-6" />
|
<Spinner className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<TileMenu
|
<TileMenu
|
||||||
desk={desk}
|
desk={desk}
|
||||||
active={active}
|
active={active}
|
||||||
menuColor={menuColor}
|
menuColor={active ? menuColor : suspendMenuColor}
|
||||||
lightText={lightText}
|
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"
|
className="absolute z-10 top-2.5 right-2.5 sm:top-4 sm:right-4 opacity-0 pointer-coarse:opacity-100 hover-none:opacity-100 focus:opacity-100 group-hover:opacity-100"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="h4 absolute z-10 bottom-3 left-1 lg:bottom-7 lg:left-5 py-1 px-3 rounded-lg"
|
className="h4 absolute z-10 bottom-[8%] left-[5%] sm:bottom-7 sm:left-5 py-1 px-3 rounded-lg"
|
||||||
style={{ backgroundColor }}
|
style={{ backgroundColor }}
|
||||||
>
|
>
|
||||||
<h3 className="mix-blend-hard-light">{title}</h3>
|
<h3 className="mix-blend-hard-light">{title}</h3>
|
||||||
|
@ -11,6 +11,10 @@ export const TileInfo = () => {
|
|||||||
const charge = useCharge(desk);
|
const charge = useCharge(desk);
|
||||||
const vat = useVat(desk);
|
const vat = useVat(desk);
|
||||||
|
|
||||||
|
if (!charge) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onOpenChange={(open) => !open && push('/')}>
|
<Dialog open onOpenChange={(open) => !open && push('/')}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { hsla, parseToHsla } from 'color2k';
|
import { darken, hsla, lighten, parseToHsla, readableColorIsBlack } from 'color2k';
|
||||||
import { useCurrentTheme } from '../state/local';
|
import { useCurrentTheme } from '../state/local';
|
||||||
|
|
||||||
function getDarkColor(color: string): string {
|
function getDarkColor(color: string): string {
|
||||||
@ -6,11 +6,31 @@ function getDarkColor(color: string): string {
|
|||||||
return hsla(hslaColor[0], hslaColor[1], 1 - hslaColor[2], 1);
|
return hsla(hslaColor[0], hslaColor[1], 1 - hslaColor[2], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bgAdjustedColor(color: string, darkBg: boolean): string {
|
||||||
|
return darkBg ? lighten(color, 0.1) : darken(color, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMenuColor(color: string, darkBg: boolean): string {
|
||||||
|
const hslaColor = parseToHsla(color);
|
||||||
|
const satAdjustedColor = hsla(hslaColor[0], Math.max(0.2, hslaColor[1]), hslaColor[2], 1);
|
||||||
|
|
||||||
|
return bgAdjustedColor(satAdjustedColor, darkBg);
|
||||||
|
}
|
||||||
|
|
||||||
export const useTileColor = (color: string) => {
|
export const useTileColor = (color: string) => {
|
||||||
const theme = useCurrentTheme();
|
const theme = useCurrentTheme();
|
||||||
|
const darkTheme = theme === 'dark';
|
||||||
|
const tileColor = darkTheme ? getDarkColor(color) : color;
|
||||||
|
const darkBg = !readableColorIsBlack(tileColor);
|
||||||
|
const lightText = darkBg !== darkTheme; // if not same, light text
|
||||||
|
const suspendColor = darkTheme ? 'rgb(26,26,26)' : 'rgb(220,220,220)';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme,
|
theme,
|
||||||
tileColor: theme === 'dark' ? getDarkColor(color) : color
|
tileColor: theme === 'dark' ? getDarkColor(color) : color,
|
||||||
|
menuColor: getMenuColor(tileColor, darkBg),
|
||||||
|
suspendColor,
|
||||||
|
suspendMenuColor: bgAdjustedColor(suspendColor, darkBg),
|
||||||
|
lightText
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
import { Box, Col } from '@tlon/indigo-react';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Helmet from 'react-helmet';
|
|
||||||
import { Route } from 'react-router-dom';
|
|
||||||
import withState from '~/logic/lib/withState';
|
|
||||||
import useHarkState from '~/logic/state/hark';
|
|
||||||
import Api from './api';
|
|
||||||
import { History } from './components/history';
|
|
||||||
import { Input } from './components/input';
|
|
||||||
import './css/custom.css';
|
|
||||||
import Store from './store';
|
|
||||||
import Subscription from './subscription';
|
|
||||||
|
|
||||||
class TermApp extends Component<any, any> {
|
|
||||||
store: Store;
|
|
||||||
api: any;
|
|
||||||
subscription: any;
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.store = new Store();
|
|
||||||
this.store.setStateHandler(this.setState.bind(this));
|
|
||||||
|
|
||||||
this.state = this.store.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetControllers() {
|
|
||||||
this.api = null;
|
|
||||||
this.subscription = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.resetControllers();
|
|
||||||
// eslint-disable-next-line new-cap
|
|
||||||
const channel = new (window as any).channel();
|
|
||||||
this.api = new Api(this.props.ship, channel);
|
|
||||||
this.store.api = this.api;
|
|
||||||
|
|
||||||
this.subscription = new Subscription(this.store, this.api, channel);
|
|
||||||
this.subscription.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.subscription.delete();
|
|
||||||
this.store.clear();
|
|
||||||
this.resetControllers();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet defer={false}>
|
|
||||||
<title>{ this.props.notificationsCount ? `(${String(this.props.notificationsCount) }) `: '' }Landscape</title>
|
|
||||||
</Helmet>
|
|
||||||
<Box
|
|
||||||
height='100%'
|
|
||||||
>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/~term/"
|
|
||||||
render={(props) => {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
width='100%'
|
|
||||||
height='100%'
|
|
||||||
display='flex'
|
|
||||||
>
|
|
||||||
<Col
|
|
||||||
p={3}
|
|
||||||
backgroundColor='white'
|
|
||||||
width='100%'
|
|
||||||
minHeight={0}
|
|
||||||
minWidth={0}
|
|
||||||
color='lightGray'
|
|
||||||
borderRadius={2}
|
|
||||||
mx={['0','3']}
|
|
||||||
mb={['0','3']}
|
|
||||||
border={['0','1']}
|
|
||||||
cursor='text'
|
|
||||||
>
|
|
||||||
{/* @ts-ignore declare props in later pass */}
|
|
||||||
<History log={this.state.lines.slice(0, -1)} />
|
|
||||||
<Input
|
|
||||||
ship={this.props.ship}
|
|
||||||
cursor={this.state.cursor}
|
|
||||||
api={this.api}
|
|
||||||
store={this.store}
|
|
||||||
line={this.state.lines.slice(-1)[0]}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withState(TermApp, [[useHarkState]]);
|
|
94
pkg/interface/webterm/app.tsx
Normal file
94
pkg/interface/webterm/app.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { Box, Col } from '@tlon/indigo-react';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import dark from '@tlon/indigo-dark';
|
||||||
|
import light from '@tlon/indigo-light';
|
||||||
|
import { ThemeProvider } from 'styled-components';
|
||||||
|
import Api from './api';
|
||||||
|
import { History } from './components/history';
|
||||||
|
import { Input } from './components/input';
|
||||||
|
import './css/custom.css';
|
||||||
|
import Store from './store';
|
||||||
|
import Subscription from './subscription';
|
||||||
|
import Channel from './lib/channel';
|
||||||
|
|
||||||
|
class TermApp extends Component<any, any> {
|
||||||
|
store: Store;
|
||||||
|
api: any;
|
||||||
|
subscription: any;
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.store = new Store();
|
||||||
|
this.store.setStateHandler(this.setState.bind(this));
|
||||||
|
|
||||||
|
this.state = this.store.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetControllers() {
|
||||||
|
this.api = null;
|
||||||
|
this.subscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.resetControllers();
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const channel = new Channel();
|
||||||
|
this.api = new Api(window.ship, channel);
|
||||||
|
this.store.api = this.api;
|
||||||
|
|
||||||
|
this.subscription = new Subscription(this.store, this.api, channel);
|
||||||
|
this.subscription.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.subscription.delete();
|
||||||
|
this.store.clear();
|
||||||
|
this.resetControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTheme() {
|
||||||
|
const { props } = this;
|
||||||
|
return ((props.dark && props?.display?.theme == 'auto') ||
|
||||||
|
props?.display?.theme == 'dark'
|
||||||
|
) ? dark : light;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const theme = this.getTheme();
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<Box
|
||||||
|
width='100%'
|
||||||
|
height='100%'
|
||||||
|
p={['0','3']}
|
||||||
|
style={{ boxSizing: 'border-box' }}
|
||||||
|
>
|
||||||
|
<Col
|
||||||
|
p={3}
|
||||||
|
backgroundColor='white'
|
||||||
|
width='100%'
|
||||||
|
height='100%'
|
||||||
|
minHeight={0}
|
||||||
|
minWidth={0}
|
||||||
|
color='lightGray'
|
||||||
|
borderRadius={2}
|
||||||
|
border={['0','1']}
|
||||||
|
cursor='text'
|
||||||
|
style={{ boxSizing: 'border-box' }}
|
||||||
|
>
|
||||||
|
{/* @ts-ignore declare props in later pass */}
|
||||||
|
<History log={this.state.lines.slice(0, -1)} />
|
||||||
|
<Input
|
||||||
|
ship={this.props.ship}
|
||||||
|
cursor={this.state.cursor}
|
||||||
|
api={this.api}
|
||||||
|
store={this.store}
|
||||||
|
line={this.state.lines.slice(-1)[0]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Box>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TermApp;
|
@ -101,14 +101,14 @@ belt = { met: 'bac' };
|
|||||||
autoFocus
|
autoFocus
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
color='black'
|
color='lightGray'
|
||||||
minHeight={0}
|
minHeight={0}
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
width='100%'
|
width='100%'
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
tabindex={0}
|
tabindex={0}
|
||||||
wrap="off"
|
wrap="off"
|
||||||
className="mono"
|
fontFamily="mono"
|
||||||
id="term"
|
id="term"
|
||||||
cursor={this.props.cursor}
|
cursor={this.props.cursor}
|
||||||
onKeyDown={this.keyPress}
|
onKeyDown={this.keyPress}
|
11
pkg/interface/webterm/config/urbitrc-sample
Normal file
11
pkg/interface/webterm/config/urbitrc-sample
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
URBIT_PIERS: [
|
||||||
|
"/Users/user/ships/zod/home",
|
||||||
|
],
|
||||||
|
herb: false,
|
||||||
|
URL: 'http://localhost:80',
|
||||||
|
/* FLEET: {
|
||||||
|
'zod': "http://localhost:8080',
|
||||||
|
'bus': 'http://localhost:8081'
|
||||||
|
} */
|
||||||
|
};
|
106
pkg/interface/webterm/config/webpack.dev.js
Normal file
106
pkg/interface/webterm/config/webpack.dev.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
|
const urbitrc = require('./urbitrc');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
const GIT_DESC = execSync('git describe --always', { encoding: 'utf8' }).trim();
|
||||||
|
|
||||||
|
let devServer = {
|
||||||
|
contentBase: path.join(__dirname, '../dist'),
|
||||||
|
hot: true,
|
||||||
|
port: 9000,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
disableHostCheck: true,
|
||||||
|
historyApiFallback: true,
|
||||||
|
publicPath: '/apps/webterm/'
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost:9000`);
|
||||||
|
|
||||||
|
if(urbitrc.URL) {
|
||||||
|
devServer = {
|
||||||
|
...devServer,
|
||||||
|
index: 'index.html',
|
||||||
|
proxy: [{
|
||||||
|
changeOrigin: true,
|
||||||
|
target: urbitrc.URL,
|
||||||
|
router,
|
||||||
|
context: (path) => {
|
||||||
|
return !path.startsWith('/apps/webterm');
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'development',
|
||||||
|
entry: {
|
||||||
|
app: './index.js'
|
||||||
|
// serviceworker: './src/serviceworker.js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(j|t)sx?$/,
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: ['@babel/preset-env', '@babel/typescript', ['@babel/preset-react', {
|
||||||
|
runtime: 'automatic',
|
||||||
|
development: true,
|
||||||
|
importSource: '@welldone-software/why-did-you-render'
|
||||||
|
}]],
|
||||||
|
plugins: [
|
||||||
|
'@babel/transform-runtime',
|
||||||
|
'@babel/plugin-proposal-object-rest-spread',
|
||||||
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
|
'@babel/plugin-proposal-class-properties',
|
||||||
|
'react-hot-loader/babel'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light|@tlon\/indigo-react)\/).*/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/i,
|
||||||
|
use: [
|
||||||
|
// Creates `style` nodes from JS strings
|
||||||
|
'style-loader',
|
||||||
|
// Translates CSS into CommonJS
|
||||||
|
'css-loader',
|
||||||
|
// Compiles Sass to CSS
|
||||||
|
'sass-loader'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts', '.tsx']
|
||||||
|
},
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
devServer: devServer,
|
||||||
|
plugins: [
|
||||||
|
// new CleanWebpackPlugin(),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
title: 'Terminal',
|
||||||
|
template: './index.html'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
watch: true,
|
||||||
|
output: {
|
||||||
|
filename: (pathData) => {
|
||||||
|
return pathData.chunk.name === 'app' ? 'index.js' : '[name].js';
|
||||||
|
},
|
||||||
|
chunkFilename: '[name].js',
|
||||||
|
path: path.resolve(__dirname, '../dist'),
|
||||||
|
publicPath: '/apps/webterm/',
|
||||||
|
globalObject: 'this'
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: false,
|
||||||
|
usedExports: true
|
||||||
|
}
|
||||||
|
};
|
77
pkg/interface/webterm/config/webpack.prod.js
Normal file
77
pkg/interface/webterm/config/webpack.prod.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
|
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
const GIT_DESC = execSync('git describe --always', { encoding: 'utf8' }).trim();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'production',
|
||||||
|
entry: {
|
||||||
|
app: './index.js',
|
||||||
|
// serviceworker: './src/serviceworker.js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(j|t)sx?$/,
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: ['@babel/preset-env', '@babel/typescript', '@babel/preset-react'],
|
||||||
|
plugins: [
|
||||||
|
'lodash',
|
||||||
|
'@babel/transform-runtime',
|
||||||
|
'@babel/plugin-proposal-object-rest-spread',
|
||||||
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
|
'@babel/plugin-proposal-class-properties'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light|@tlon\/indigo-react)\/).*/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/i,
|
||||||
|
use: [
|
||||||
|
// Creates `style` nodes from JS strings
|
||||||
|
'style-loader',
|
||||||
|
// Translates CSS into CommonJS
|
||||||
|
'css-loader',
|
||||||
|
// Compiles Sass to CSS
|
||||||
|
'sass-loader'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts', '.tsx']
|
||||||
|
},
|
||||||
|
devtool: 'source-map',
|
||||||
|
// devServer: {
|
||||||
|
// contentBase: path.join(__dirname, './'),
|
||||||
|
// hot: true,
|
||||||
|
// port: 9000,
|
||||||
|
// historyApiFallback: true
|
||||||
|
// },
|
||||||
|
plugins: [
|
||||||
|
new MomentLocalesPlugin(),
|
||||||
|
new CleanWebpackPlugin(),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
title: 'Terminal',
|
||||||
|
template: './index.html'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
filename: (pathData) => {
|
||||||
|
return pathData.chunk.name === 'app' ? 'index.[contenthash].js' : '[name].js';
|
||||||
|
},
|
||||||
|
path: path.resolve(__dirname, '../dist'),
|
||||||
|
publicPath: '/apps/webterm/'
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
usedExports: true
|
||||||
|
}
|
||||||
|
};
|
@ -1,6 +1,13 @@
|
|||||||
|
body, #root {
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
input#term {
|
input#term {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blink {
|
.blink {
|
26
pkg/interface/webterm/index.html
Normal file
26
pkg/interface/webterm/index.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Terminal</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, shrink-to-fit=no,maximum-scale=1"/>
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-touch-fullscreen" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<link rel="apple-touch-icon" href="/~landscape/img/touch_icon.png">
|
||||||
|
<link rel="icon" type="image/png" href="/~landscape/img/Favicon.png">
|
||||||
|
<link rel="manifest"
|
||||||
|
href='data:application/manifest+json,{
|
||||||
|
"name": "Terminal",
|
||||||
|
"short_name": "Terminal",
|
||||||
|
"description": "A%20terminal%20for%20your%20Urbit.",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "%23FFFFFF",
|
||||||
|
"theme_color": "%23000000"}' />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/session.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
5
pkg/interface/webterm/index.js
Normal file
5
pkg/interface/webterm/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import * as ReactDOM from 'react-dom';
|
||||||
|
import TermApp from './App';
|
||||||
|
|
||||||
|
ReactDOM.render(<TermApp />, document.getElementById('root'));
|
290
pkg/interface/webterm/lib/channel.js
Normal file
290
pkg/interface/webterm/lib/channel.js
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
export default class Channel {
|
||||||
|
constructor() {
|
||||||
|
this.init();
|
||||||
|
this.deleteOnUnload();
|
||||||
|
|
||||||
|
// a way to handle channel errors
|
||||||
|
//
|
||||||
|
//
|
||||||
|
this.onChannelError = (err) => {
|
||||||
|
console.error('event source error: ', err);
|
||||||
|
};
|
||||||
|
this.onChannelOpen = (e) => {
|
||||||
|
console.log('open', e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.debounceInterval = 500;
|
||||||
|
// unique identifier: current time and random number
|
||||||
|
//
|
||||||
|
this.uid =
|
||||||
|
new Date().getTime().toString() +
|
||||||
|
"-" +
|
||||||
|
Math.random().toString(16).slice(-6);
|
||||||
|
|
||||||
|
this.requestId = 1;
|
||||||
|
|
||||||
|
// the currently connected EventSource
|
||||||
|
//
|
||||||
|
this.eventSource = null;
|
||||||
|
|
||||||
|
// the id of the last EventSource event we received
|
||||||
|
//
|
||||||
|
this.lastEventId = 0;
|
||||||
|
|
||||||
|
// this last event id acknowledgment sent to the server
|
||||||
|
//
|
||||||
|
this.lastAcknowledgedEventId = 0;
|
||||||
|
|
||||||
|
// a registry of requestId to successFunc/failureFunc
|
||||||
|
//
|
||||||
|
// These functions are registered during a +poke and are executed
|
||||||
|
// in the onServerEvent()/onServerError() callbacks. Only one of
|
||||||
|
// the functions will be called, and the outstanding poke will be
|
||||||
|
// removed after calling the success or failure function.
|
||||||
|
//
|
||||||
|
|
||||||
|
this.outstandingPokes = new Map();
|
||||||
|
|
||||||
|
// a registry of requestId to subscription functions.
|
||||||
|
//
|
||||||
|
// These functions are registered during a +subscribe and are
|
||||||
|
// executed in the onServerEvent()/onServerError() callbacks. The
|
||||||
|
// event function will be called whenever a new piece of data on this
|
||||||
|
// subscription is available, which may be 0, 1, or many times. The
|
||||||
|
// disconnect function may be called exactly once.
|
||||||
|
//
|
||||||
|
this.outstandingSubscriptions = new Map();
|
||||||
|
|
||||||
|
this.outstandingJSON = [];
|
||||||
|
|
||||||
|
this.debounceTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDebounceTimer() {
|
||||||
|
if (this.debounceTimer) {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
this.debounceTimer = null;
|
||||||
|
}
|
||||||
|
this.debounceTimer = setTimeout(() => {
|
||||||
|
this.sendJSONToChannel();
|
||||||
|
}, this.debounceInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnChannelError(onError = (err) => {}) {
|
||||||
|
this.onChannelError = onError;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnChannelOpen(onOpen = (e) => {}) {
|
||||||
|
this.onChannelOpen = onOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteOnUnload() {
|
||||||
|
window.addEventListener("beforeunload", (event) => {
|
||||||
|
this.delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearQueue() {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
this.debounceTimer = null;
|
||||||
|
this.sendJSONToChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends a poke to an app on an urbit ship
|
||||||
|
//
|
||||||
|
poke(ship, app, mark, json, successFunc, failureFunc) {
|
||||||
|
let id = this.nextId();
|
||||||
|
this.outstandingPokes.set(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
success: successFunc,
|
||||||
|
fail: failureFunc
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const j = {
|
||||||
|
id,
|
||||||
|
action: "poke",
|
||||||
|
ship,
|
||||||
|
app,
|
||||||
|
mark,
|
||||||
|
json
|
||||||
|
};
|
||||||
|
|
||||||
|
this.sendJSONToChannel(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
// subscribes to a path on an specific app and ship.
|
||||||
|
//
|
||||||
|
// Returns a subscription id, which is the same as the same internal id
|
||||||
|
// passed to your Urbit.
|
||||||
|
subscribe(
|
||||||
|
ship,
|
||||||
|
app,
|
||||||
|
path,
|
||||||
|
connectionErrFunc = () => {},
|
||||||
|
eventFunc = () => {},
|
||||||
|
quitFunc = () => {},
|
||||||
|
subAckFunc = () => {},
|
||||||
|
) {
|
||||||
|
let id = this.nextId();
|
||||||
|
this.outstandingSubscriptions.set(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
err: connectionErrFunc,
|
||||||
|
event: eventFunc,
|
||||||
|
quit: quitFunc,
|
||||||
|
subAck: subAckFunc
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const json = {
|
||||||
|
id,
|
||||||
|
action: "subscribe",
|
||||||
|
ship,
|
||||||
|
app,
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resetDebounceTimer();
|
||||||
|
|
||||||
|
this.outstandingJSON.push(json);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// quit the channel
|
||||||
|
//
|
||||||
|
delete() {
|
||||||
|
let id = this.nextId();
|
||||||
|
clearInterval(this.ackTimer);
|
||||||
|
navigator.sendBeacon(this.channelURL(), JSON.stringify([{
|
||||||
|
id,
|
||||||
|
action: "delete"
|
||||||
|
}]));
|
||||||
|
if (this.eventSource) {
|
||||||
|
this.eventSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsubscribe to a specific subscription
|
||||||
|
//
|
||||||
|
unsubscribe(subscription) {
|
||||||
|
let id = this.nextId();
|
||||||
|
this.sendJSONToChannel({
|
||||||
|
id,
|
||||||
|
action: "unsubscribe",
|
||||||
|
subscription
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends a JSON command command to the server.
|
||||||
|
//
|
||||||
|
sendJSONToChannel(j) {
|
||||||
|
let req = new XMLHttpRequest();
|
||||||
|
req.open("PUT", this.channelURL());
|
||||||
|
req.setRequestHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
|
if (this.lastEventId == this.lastAcknowledgedEventId) {
|
||||||
|
if (j) {
|
||||||
|
this.outstandingJSON.push(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.outstandingJSON.length > 0) {
|
||||||
|
let x = JSON.stringify(this.outstandingJSON);
|
||||||
|
req.send(x);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we add an acknowledgment to clear the server side queue
|
||||||
|
//
|
||||||
|
// The server side puts messages it sends us in a queue until we
|
||||||
|
// acknowledge that we received it.
|
||||||
|
//
|
||||||
|
let payload = [
|
||||||
|
...this.outstandingJSON,
|
||||||
|
{action: "ack", "event-id": this.lastEventId}
|
||||||
|
];
|
||||||
|
if (j) {
|
||||||
|
payload.push(j)
|
||||||
|
}
|
||||||
|
let x = JSON.stringify(payload);
|
||||||
|
req.send(x);
|
||||||
|
|
||||||
|
this.lastAcknowledgedEventId = this.lastEventId;
|
||||||
|
}
|
||||||
|
this.outstandingJSON = [];
|
||||||
|
|
||||||
|
this.connectIfDisconnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
// connects to the EventSource if we are not currently connected
|
||||||
|
//
|
||||||
|
connectIfDisconnected() {
|
||||||
|
if (this.eventSource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventSource = new EventSource(this.channelURL(), {withCredentials:true});
|
||||||
|
this.eventSource.onmessage = e => {
|
||||||
|
this.lastEventId = parseInt(e.lastEventId, 10);
|
||||||
|
|
||||||
|
let obj = JSON.parse(e.data);
|
||||||
|
let pokeFuncs = this.outstandingPokes.get(obj.id);
|
||||||
|
let subFuncs = this.outstandingSubscriptions.get(obj.id);
|
||||||
|
|
||||||
|
if (obj.response == "poke" && !!pokeFuncs) {
|
||||||
|
let funcs = pokeFuncs;
|
||||||
|
if (obj.hasOwnProperty("ok")) {
|
||||||
|
funcs["success"]();
|
||||||
|
} else if (obj.hasOwnProperty("err")) {
|
||||||
|
funcs["fail"](obj.err);
|
||||||
|
} else {
|
||||||
|
console.error("Invalid poke response: ", obj);
|
||||||
|
}
|
||||||
|
this.outstandingPokes.delete(obj.id);
|
||||||
|
|
||||||
|
} else if (obj.response == "subscribe" ||
|
||||||
|
(obj.response == "poke" && !!subFuncs)) {
|
||||||
|
let funcs = subFuncs;
|
||||||
|
|
||||||
|
if (obj.hasOwnProperty("err")) {
|
||||||
|
funcs["err"](obj.err);
|
||||||
|
this.outstandingSubscriptions.delete(obj.id);
|
||||||
|
} else if (obj.hasOwnProperty("ok")) {
|
||||||
|
funcs["subAck"](obj);
|
||||||
|
}
|
||||||
|
} else if (obj.response == "diff") {
|
||||||
|
// ensure we ack before channel clogs
|
||||||
|
if((this.lastEventId - this.lastAcknowledgedEventId) > 30) {
|
||||||
|
this.clearQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
let funcs = subFuncs;
|
||||||
|
funcs["event"](obj.json);
|
||||||
|
} else if (obj.response == "quit") {
|
||||||
|
let funcs = subFuncs;
|
||||||
|
funcs["quit"](obj);
|
||||||
|
this.outstandingSubscriptions.delete(obj.id);
|
||||||
|
} else {
|
||||||
|
console.log("Unrecognized response: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventSource.onopen = this.onChannelOpen;
|
||||||
|
|
||||||
|
this.eventSource.onerror = e => {
|
||||||
|
this.delete();
|
||||||
|
this.init();
|
||||||
|
this.onChannelError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channelURL() {
|
||||||
|
return "/~/channel/" + this.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextId() {
|
||||||
|
return this.requestId++;
|
||||||
|
}
|
||||||
|
}
|
24922
pkg/interface/webterm/package-lock.json
generated
Normal file
24922
pkg/interface/webterm/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
134
pkg/interface/webterm/package.json
Normal file
134
pkg/interface/webterm/package.json
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"name": "interface",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@reach/disclosure": "^0.10.5",
|
||||||
|
"@reach/menu-button": "^0.10.5",
|
||||||
|
"@reach/tabs": "^0.10.5",
|
||||||
|
"@react-spring/web": "^9.1.1",
|
||||||
|
"@tlon/indigo-dark": "^1.0.6",
|
||||||
|
"@tlon/indigo-light": "^1.0.7",
|
||||||
|
"@tlon/indigo-react": "^1.2.23",
|
||||||
|
"@tlon/sigil-js": "^1.4.3",
|
||||||
|
"@urbit/api": "^1.1.1",
|
||||||
|
"@urbit/http-api": "^1.2.1",
|
||||||
|
"any-ascii": "^0.1.7",
|
||||||
|
"aws-sdk": "^2.830.0",
|
||||||
|
"big-integer": "^1.6.48",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"codemirror": "^5.59.2",
|
||||||
|
"css-loader": "^3.6.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"formik": "^2.1.5",
|
||||||
|
"immer": "^9.0.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"moment": "^2.29.1",
|
||||||
|
"mousetrap": "^1.6.5",
|
||||||
|
"mousetrap-global-bind": "^1.1.0",
|
||||||
|
"normalize-wheel": "1.0.1",
|
||||||
|
"oembed-parser": "^1.4.5",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"querystring": "^0.2.0",
|
||||||
|
"react": "^16.14.0",
|
||||||
|
"react-codemirror2": "^6.0.1",
|
||||||
|
"react-dom": "^16.14.0",
|
||||||
|
"react-helmet": "^6.1.0",
|
||||||
|
"react-markdown": "^4.3.1",
|
||||||
|
"react-oembed-container": "^1.0.0",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"react-use-gesture": "^9.1.3",
|
||||||
|
"react-virtuoso": "^0.20.3",
|
||||||
|
"react-visibility-sensor": "^5.1.1",
|
||||||
|
"remark": "^12.0.0",
|
||||||
|
"remark-breaks": "^2.0.2",
|
||||||
|
"remark-disable-tokenizers": "1.1.0",
|
||||||
|
"stacktrace-js": "^2.0.2",
|
||||||
|
"style-loader": "^1.3.0",
|
||||||
|
"styled-components": "^5.1.1",
|
||||||
|
"styled-system": "^5.1.5",
|
||||||
|
"suncalc": "^1.8.0",
|
||||||
|
"unist-util-visit": "^3.0.0",
|
||||||
|
"urbit-ob": "^5.0.1",
|
||||||
|
"workbox-core": "^6.0.2",
|
||||||
|
"workbox-precaching": "^6.0.2",
|
||||||
|
"workbox-recipes": "^6.0.2",
|
||||||
|
"workbox-routing": "^6.0.2",
|
||||||
|
"yup": "^0.29.3",
|
||||||
|
"zustand": "^3.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.12.10",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||||
|
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||||
|
"@babel/plugin-proposal-optional-chaining": "^7.12.7",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||||
|
"@babel/preset-env": "^7.12.11",
|
||||||
|
"@babel/preset-react": "^7.12.10",
|
||||||
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
|
"@storybook/addon-actions": "^6.2.9",
|
||||||
|
"@storybook/addon-essentials": "^6.2.9",
|
||||||
|
"@storybook/addon-links": "^6.2.9",
|
||||||
|
"@storybook/react": "^6.2.9",
|
||||||
|
"@types/lodash": "^4.14.168",
|
||||||
|
"@types/react": "^16.14.2",
|
||||||
|
"@types/react-dom": "^16.9.10",
|
||||||
|
"@types/react-router-dom": "^5.1.7",
|
||||||
|
"@types/styled-components": "^5.1.7",
|
||||||
|
"@types/styled-system": "^5.1.10",
|
||||||
|
"@types/yup": "^0.29.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.15.0",
|
||||||
|
"@typescript-eslint/parser": "^4.24.0",
|
||||||
|
"@urbit/eslint-config": "^1.0.0",
|
||||||
|
"@welldone-software/why-did-you-render": "^6.1.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"babel-jest": "^26.6.3",
|
||||||
|
"babel-loader": "^8.2.2",
|
||||||
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
|
"babel-plugin-root-import": "^6.6.0",
|
||||||
|
"chromatic": "^5.8.3",
|
||||||
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^7.26.0",
|
||||||
|
"eslint-plugin-react": "^7.22.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"html-webpack-plugin": "^4.5.1",
|
||||||
|
"husky": "^6.0.0",
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"lint-staged": "^11.0.0",
|
||||||
|
"loki": "^0.28.1",
|
||||||
|
"moment-locales-webpack-plugin": "^1.2.0",
|
||||||
|
"react-hot-loader": "^4.13.0",
|
||||||
|
"sass": "^1.32.5",
|
||||||
|
"sass-loader": "^8.0.2",
|
||||||
|
"storybook-addon-designs": "^6.0.0",
|
||||||
|
"ts-mdast": "^1.0.0",
|
||||||
|
"typescript": "^4.2.4",
|
||||||
|
"webpack": "^4.46.0",
|
||||||
|
"webpack-cli": "^3.3.12",
|
||||||
|
"webpack-dev-server": "^3.11.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint ./src/**/*.{ts,tsx}",
|
||||||
|
"lint-file": "eslint",
|
||||||
|
"tsc": "tsc",
|
||||||
|
"tsc:watch": "tsc --watch",
|
||||||
|
"build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
|
||||||
|
"build:prod": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
|
||||||
|
"start": "webpack-dev-server --config config/webpack.dev.js",
|
||||||
|
"test": "tsc && jest",
|
||||||
|
"jest": "jest",
|
||||||
|
"storybook": "start-storybook -p 6006",
|
||||||
|
"build-storybook": "build-storybook",
|
||||||
|
"chromatic": "chromatic --exit-zero-on-changes",
|
||||||
|
"hook-lint": "eslint --cache --fix"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,ts,tsx}": "eslint --cache --fix"
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import bel from '../../../logic/lib/bel';
|
import bel from './lib/bel';
|
||||||
|
|
||||||
export default class Store {
|
export default class Store {
|
||||||
state: any;
|
state: any;
|
1694
pkg/npm/api/package-lock.json
generated
1694
pkg/npm/api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
2312
pkg/npm/eslint-config/package-lock.json
generated
2312
pkg/npm/eslint-config/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14352
pkg/npm/http-api/package-lock.json
generated
14352
pkg/npm/http-api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
1
pkg/webterm/desk.bill
Normal file
1
pkg/webterm/desk.bill
Normal file
@ -0,0 +1 @@
|
|||||||
|
~
|
9
pkg/webterm/desk.docket
Normal file
9
pkg/webterm/desk.docket
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
:~ title+'Web Terminal'
|
||||||
|
info+'A web interface for dill, through herm.'
|
||||||
|
color+0xff.ffff
|
||||||
|
glob-http+'https://bootstrap.urbit.org/glob-0v4.8ui32.ui10d.t0v4d.n9g1s.1ftua.glob'
|
||||||
|
base+'webterm'
|
||||||
|
version+[0 0 1]
|
||||||
|
website+'https://tlon.io'
|
||||||
|
license+'MIT'
|
||||||
|
==
|
1
pkg/webterm/sys.kelvin
Normal file
1
pkg/webterm/sys.kelvin
Normal file
@ -0,0 +1 @@
|
|||||||
|
[%zuse 420]
|
Loading…
Reference in New Issue
Block a user