grid: adding installed apps search and lots of little tweaks

This commit is contained in:
Hunter Miller 2021-09-25 15:32:37 -05:00
parent 2b27425dfe
commit f62420ea17
11 changed files with 141 additions and 44 deletions

View File

@ -39,7 +39,7 @@ export const ProviderList = ({
return (
<ul
className={classNames(size !== 'default' ? 'space-y-4' : 'space-y-8', listClass)}
className={classNames('-mx-2', size !== 'default' ? 'space-y-4' : 'space-y-8', listClass)}
aria-labelledby={labelledBy}
>
{providers.map((p) => (

View File

@ -70,6 +70,14 @@ export const Leap = React.forwardRef(
}
}, [selection, rawInput, appsMatch]);
useEffect(() => {
const newMatch = getMatch(rawInput);
if (newMatch && rawInput) {
useLeapStore.setState({ selectedMatch: newMatch });
}
}, [rawInput, matches]);
const toggleSearch = useCallback(() => {
if (selection || menu === 'search') {
return;
@ -193,6 +201,9 @@ export const Leap = React.forwardRef(
if (arrow) {
e.preventDefault();
if (matches.length === 0) {
return;
}
const currentIndex = selectedMatch
? matches.findIndex((m) => {

View File

@ -148,7 +148,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
<DialogContent
onInteractOutside={preventClose}
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 justify-end sm:justify-start scroll-full-width h-full max-w-[882px] px-4 sm:pb-4 text-gray-400 -translate-x-1/2 outline-none"
role="combobox"
aria-controls="leap-items"
aria-owns="leap-items"
@ -161,7 +161,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
/>
<div
id="leap-items"
className="grid grid-rows-[fit-content(calc(100vh-6.25rem))] bg-white rounded-3xl overflow-hidden default-ring focus-visible:ring-2"
className="grid grid-rows-[fit-content(calc(100vh-6.25rem))] mt-4 sm:mt-0 bg-white rounded-3xl overflow-hidden default-ring focus-visible:ring-2"
tabIndex={0}
role="listbox"
>

View File

@ -55,7 +55,7 @@ export const Notifications = ({ history }: RouteComponentProps) => {
variant="secondary"
className="flex-auto sm:flex-none py-1.5 px-2 sm:px-6 text-sm sm:text-base rounded-full"
>
Mark All as Read
Archive All
</Button>
<Button
as={Link}

View File

@ -1,10 +1,11 @@
import classNames from 'classnames';
import React from 'react';
import React, { useCallback } from 'react';
import { Timebox } from '@urbit/api';
import { Link, LinkProps } from 'react-router-dom';
import { Bullet } from '../components/icons/Bullet';
import { Cross } from '../components/icons/Cross';
import { useHarkStore } from '../state/hark';
import { useLeapStore } from './Nav';
type NotificationsState = 'empty' | 'unread' | 'attention-needed' | 'open';
@ -42,6 +43,8 @@ export const NotificationsLink = ({
}: NotificationsLinkProps) => {
const unseen = useHarkStore((s) => s.unseen);
const state = getNotificationsState(notificationsOpen, unseen);
const select = useLeapStore((s) => s.select);
const clearSelection = useCallback(() => select(null), [select]);
return (
<Link
@ -56,6 +59,7 @@ export const NotificationsLink = ({
state === 'unread' && 'bg-blue-400 text-white',
state === 'attention-needed' && 'text-white bg-orange-400'
)}
onClick={clearSelection}
>
{state === 'empty' && <Bullet className="w-6 h-6" />}
{state === 'unread' && Object.keys(unseen).length}

View File

@ -9,6 +9,7 @@ import { useVat } from '../state/kiln';
import { disableDefault, handleDropdownLink } from '../state/util';
import { useMedia } from '../logic/useMedia';
import { Cross } from '../components/icons/Cross';
import { useLeapStore } from './Nav';
type SystemMenuProps = HTMLAttributes<HTMLButtonElement> & {
open: boolean;
@ -27,6 +28,8 @@ export const SystemMenu = ({ className, open, subMenuOpen, shouldDim }: SystemMe
const garden = useVat(window.desk);
const hash = garden ? getHash(garden) : null;
const isMobile = useMedia('(max-width: 639px)');
const select = useLeapStore((s) => s.select);
const clearSelection = useCallback(() => select(null), [select]);
const copyHash = useCallback(
(event: Event) => {
@ -69,6 +72,7 @@ export const SystemMenu = ({ className, open, subMenuOpen, shouldDim }: SystemMe
shouldDim && 'opacity-60',
className
)}
onClick={clearSelection}
>
{!open && !subMenuOpen && (
<>

View File

@ -85,7 +85,7 @@ export const Home = () => {
const selectedMatch = useLeapStore((state) => state.selectedMatch);
const { recentApps, recentDevs } = useRecentsStore();
const charges = useCharges();
const groups = charges?.groups;
const groups = charges?.landscape;
const contacts = useContactState((s) => s.contacts);
const defaultAlly = useDocketState((s) =>
s.defaultAlly ? { shipName: s.defaultAlly, ...contacts[s.defaultAlly] } : null
@ -113,10 +113,17 @@ export const Home = () => {
Recent Apps
</h2>
{apps.length === 0 && (
<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-6">You can click/tap/keyboard on a listed app to open it.</p>
{groups && <AppLink app={groups} size="small" onClick={() => addRecentApp('groups')} />}
<div className="p-6 rounded-xl bg-gray-50">
<p>Apps you use will be listed here, in the order you used them.</p>
<p className="mt-4">You can click/tap/keyboard on a listed app to open it.</p>
{groups && (
<AppLink
app={groups}
size="small"
className="mt-6"
onClick={() => addRecentApp('groups')}
/>
)}
</div>
)}
{apps.length > 0 && (
@ -127,7 +134,7 @@ export const Home = () => {
Recent Developers
</h2>
{recentDevs.length === 0 && (
<div className="min-h-[150px] p-6 rounded-xl bg-gray-50">
<div className="p-6 rounded-xl bg-gray-50">
<p className="mb-4">Urbit app developers you search for will be listed here.</p>
{defaultAlly && (
<>

View File

@ -3,9 +3,11 @@ import { RouteComponentProps } from 'react-router-dom';
import fuzzy from 'fuzzy';
import { Provider } from '@urbit/api';
import { MatchItem, useLeapStore } from '../Nav';
import { useAllies } from '../../state/docket';
import { useAllies, useCharges } from '../../state/docket';
import { ProviderList } from '../../components/ProviderList';
import useContactState from '../../state/contact';
import { AppList } from '../../components/AppList';
import { getAppHref } from '../../state/util';
type ProvidersProps = RouteComponentProps<{ ship: string }>;
@ -21,12 +23,36 @@ export function providerMatch(provider: Provider | string): MatchItem {
};
}
function fuzzySort(search: string) {
return (a: fuzzy.FilterResult<string>, b: fuzzy.FilterResult<string>): number => {
const left = a.string.startsWith(search) ? a.score + 1 : a.score;
const right = b.string.startsWith(search) ? b.score + 1 : b.score;
return right - left;
};
}
export const Providers = ({ match }: ProvidersProps) => {
const selectedMatch = useLeapStore((state) => state.selectedMatch);
const provider = match?.params.ship;
const contacts = useContactState((s) => s.contacts);
const charges = useCharges();
const allies = useAllies();
const search = provider || '';
const chargeArray = Object.entries(charges);
const appResults = useMemo(
() =>
charges
? fuzzy
.filter(
search,
chargeArray.map(([desk, charge]) => charge.title + desk)
)
.sort(fuzzySort(search))
.map((el) => chargeArray[el.index][1])
: [],
[charges, search]
);
const results = useMemo(
() =>
allies
@ -35,12 +61,7 @@ export const Providers = ({ match }: ProvidersProps) => {
search,
Object.entries(allies).map(([ship]) => ship)
)
.sort((a, b) => {
const left = a.string.startsWith(search) ? a.score + 1 : a.score;
const right = b.string.startsWith(search) ? b.score + 1 : b.score;
return right - left;
})
.sort(fuzzySort(search))
.map((el) => ({ shipName: el.original, ...contacts[el.original] }))
: [],
[allies, search, contacts]
@ -48,24 +69,63 @@ export const Providers = ({ match }: ProvidersProps) => {
const count = results?.length;
useEffect(() => {
if (search) {
useLeapStore.setState({ rawInput: search });
}
}, []);
useEffect(() => {
if (results) {
const providerMatches = results ? results.map(providerMatch) : [];
const appMatches = appResults
? appResults.map((app) => ({
url: getAppHref(app.href),
openInNewTab: true,
value: app.desk,
display: app.title
}))
: [];
useLeapStore.setState({
matches: results.map(providerMatch)
matches: ([] as MatchItem[]).concat(appMatches, providerMatches)
});
}
}, [results]);
return (
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400" aria-live="polite">
<div id="providers">
<h2 className="mb-3">Searching Software Providers</h2>
<p>
{count} result{count === 1 ? '' : 's'}
</p>
</div>
{results && (
<ProviderList providers={results} labelledBy="providers" matchAgainst={selectedMatch} />
<div
className="dialog-inner-container md:px-6 md:py-8 space-y-0 h4 text-gray-400"
aria-live="polite"
>
{appResults && !(results?.length > 0 && appResults.length === 0) && (
<div>
<h2 id="installed" className="mb-3">
Installed Apps
</h2>
<AppList
apps={appResults}
labelledBy="installed"
matchAgainst={selectedMatch}
listClass="mb-6"
/>
</div>
)}
{results && !(appResults?.length > 0 && results.length === 0) && (
<div>
<div id="providers">
<h2 className="mb-1">Searching Software Providers</h2>
<p className="mb-3">
{count} result{count === 1 ? '' : 's'}
</p>
</div>
<ProviderList
providers={results}
labelledBy="providers"
matchAgainst={selectedMatch}
listClass="mb-6"
/>
</div>
)}
<p>That&apos;s it!</p>
</div>

View File

@ -21,6 +21,7 @@ export const TreatyInfo = () => {
useEffect(() => {
select(<>{treaty?.title}</>);
useLeapStore.setState({ matches: [] });
}, [treaty?.title]);
if (!treaty) {

View File

@ -7,6 +7,8 @@ import { getAppHref } from '../state/util';
import { useRecentsStore } from '../nav/search/Home';
import { ChargeWithDesk } from '../state/docket';
import { useTileColor } from './useTileColor';
import { useVat } from '../state/kiln';
import { Bullet } from '../components/icons/Bullet';
type TileProps = {
charge: ChargeWithDesk;
@ -16,6 +18,7 @@ type TileProps = {
export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
const { title, image, color, chad, href } = charge;
const vat = useVat(desk);
const { lightText, tileColor, menuColor, suspendColor, suspendMenuColor } = useTileColor(color);
const loading = 'install' in chad;
const suspended = 'suspend' in chad;
@ -42,13 +45,18 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk }) => {
<Spinner className="h-6 w-6" />
</div>
) : (
<TileMenu
desk={desk}
active={active}
menuColor={active ? menuColor : suspendMenuColor}
lightText={lightText}
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"
/>
<>
{vat?.arak.rail?.paused && (
<Bullet className="absolute z-10 top-5 left-5 sm:top-6 sm:left-7 w-4 h-4 text-orange-500 dark:text-black" />
)}
<TileMenu
desk={desk}
active={active}
menuColor={active ? menuColor : suspendMenuColor}
lightText={lightText}
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
className="h4 absolute z-10 bottom-[8%] left-[5%] sm:bottom-7 sm:left-5 py-1 px-3 rounded-lg"

View File

@ -5,22 +5,23 @@ import reactRefresh from '@vitejs/plugin-react-refresh';
import { urbitPlugin } from '@urbit/vite-plugin-urbit';
import { execSync } from 'child_process';
// using current commit until release
const GIT_DESC = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
process.env.VITE_SHORTHASH = GIT_DESC;
// https://vitejs.dev/config/
export default ({ mode }) => {
if (mode !== 'mock') {
// using current commit until release
const GIT_DESC = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
process.env.VITE_SHORTHASH = GIT_DESC;
} else {
process.env.VITE_SHORTHASH = '1';
}
Object.assign(process.env, loadEnv(mode, process.cwd()));
const SHIP_URL = process.env.SHIP_URL || process.env.VITE_SHIP_URL || 'http://localhost:8080';
console.log(SHIP_URL);
return defineConfig({
base: mode === 'mock' ? undefined : '/apps/grid/',
server:
mode === 'mock'
? undefined
: { https: true },
server: mode === 'mock' ? undefined : { https: true },
build:
mode !== 'profile'
? undefined
@ -34,6 +35,7 @@ export default ({ mode }) => {
]
}
},
plugins: [urbitPlugin({ base: 'grid', target: SHIP_URL }), reactRefresh()]
plugins:
mode === 'mock' ? [] : [urbitPlugin({ base: 'grid', target: SHIP_URL }), reactRefresh()]
});
};