refactor(vpn-desktop): clean ui (#4263)
* set cursor default on disabled state * fix long error messages overflow * clean code * fix hop select component style * fix exit select button * add nym vpn icon * style refactoring * style refactoring * fix hover bg country list in light mode * fix hover bg country list in light mode --------- Co-authored-by: Zane Schepke <zanecschepke@gmail.com>
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="text-[12px]">
|
||||
<html lang="en" class="text-[14px]">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 25 KiB |
@@ -66,22 +66,26 @@ function ConnectionStatus() {
|
||||
{getStatusText(state.state)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-col flex-1 text-center">
|
||||
<div className="w-full flex flex-col flex-1 items-center overflow-hidden">
|
||||
{state.loading && state.progressMessages.length > 0 && !state.error && (
|
||||
<p className="text-dim-gray dark:text-mercury-mist font-bold">
|
||||
{t(
|
||||
`connection-progress.${
|
||||
state.progressMessages[state.progressMessages.length - 1]
|
||||
}`,
|
||||
{
|
||||
ns: 'backendMessages',
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<div className="w-4/5 h-2/3 overflow-scroll break-words text-center">
|
||||
<p className="text-dim-gray dark:text-mercury-mist font-bold">
|
||||
{t(
|
||||
`connection-progress.${
|
||||
state.progressMessages[state.progressMessages.length - 1]
|
||||
}`,
|
||||
{
|
||||
ns: 'backendMessages',
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{state.state === 'Connected' && <ConnectionTimer />}
|
||||
{state.error && (
|
||||
<p className="text-teaberry font-bold">{state.error}</p>
|
||||
<div className="w-4/5 h-2/3 overflow-scroll break-words text-center">
|
||||
<p className="text-teaberry font-bold">{state.error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,9 +71,8 @@ function Home() {
|
||||
<div className="h-full flex flex-col p-4">
|
||||
<ConnectionStatus />
|
||||
<div className="flex grow flex-col justify-between gap-y-2">
|
||||
<div className="flex flex-col justify-between">
|
||||
<div className="flex flex-col justify-between gap-y-2">
|
||||
<NetworkModeSelect />
|
||||
<div className="py-2"></div>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="mt-3 font-semibold text-lg">
|
||||
{t('select-node-title')}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
import { Country, NodeHop } from '../../types';
|
||||
import { QuickConnectPrefix } from '../../constants';
|
||||
import { useMainState } from '../../contexts';
|
||||
|
||||
interface HopSelectProps {
|
||||
country: Country;
|
||||
@@ -13,44 +14,41 @@ export default function HopSelect({
|
||||
country,
|
||||
onClick,
|
||||
}: HopSelectProps) {
|
||||
const { state } = useMainState();
|
||||
const { t } = useTranslation('home');
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="relative w-full flex flex-row justify-center cursor-pointer"
|
||||
onKeyDown={onClick}
|
||||
role="presentation"
|
||||
onClick={onClick}
|
||||
>
|
||||
<input
|
||||
readOnly={true}
|
||||
type="text"
|
||||
id="floating_outlined"
|
||||
value={country.name}
|
||||
className="dark:bg-baltic-sea cursor-pointer pl-11 dark:placeholder-white border border-gun-powder block px-2.5 pb-4 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer"
|
||||
/>
|
||||
<div className="top-1/2 transform -translate-y-1/2 left-2 absolute pointer-events-none">
|
||||
{country.name.includes(QuickConnectPrefix) ? (
|
||||
<span className="font-icon cursor-pointer px-2">bolt</span>
|
||||
) : (
|
||||
<img
|
||||
src={`./flags/${country.code.toLowerCase()}.svg`}
|
||||
className="h-8 scale-75 pointer-events-none fill-current"
|
||||
alt={country.code}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="font-icon scale-125 pointer-events-none absolute fill-current top-1/4 transform -translate-x-1/2 right-2">
|
||||
arrow_right
|
||||
</span>
|
||||
<label
|
||||
htmlFor="floating_outlined"
|
||||
className="dark:text-white bg-blanc-nacre dark:bg-baltic-sea absolute text-sm text-gray-500 dark:text-gray-400 ml-4 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] dark:bg-gray-900 px-2 peer-placeholder-shown:px-2 peer-placeholder-shown:text-blue-600 peer-placeholder-shown:dark:text-blue-500 peer-placeholder-shown:top-2 peer-placeholder-shown:scale-75 peer-placeholder-shown:-translate-y-4 rtl:peer-placeholder-shown:translate-x-1/4 rtl:peer-placeholder-shown:left-auto start-1"
|
||||
>
|
||||
{nodeHop === 'entry' ? t('first-hop') : t('last-hop')}
|
||||
</label>
|
||||
return (
|
||||
<div
|
||||
className={clsx([
|
||||
state === 'Disconnected' ? 'cursor-pointer' : 'cursor-not-allowed',
|
||||
'w-full flex flex-row justify-between items-center py-3 px-4',
|
||||
'text-baltic-sea dark:text-mercury-pinkish',
|
||||
'border-cement-feet dark:border-gun-powder border-2 rounded-lg',
|
||||
'relative',
|
||||
])}
|
||||
onKeyDown={onClick}
|
||||
role="presentation"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className={clsx([
|
||||
'absolute left-3 -top-3 px-1',
|
||||
'bg-blanc-nacre dark:bg-baltic-sea text-sm',
|
||||
])}
|
||||
>
|
||||
{nodeHop === 'entry' ? t('first-hop') : t('last-hop')}
|
||||
</div>
|
||||
</>
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<img
|
||||
src={`./flags/${country.code.toLowerCase()}.svg`}
|
||||
className="h-8 scale-90 pointer-events-none fill-current"
|
||||
alt={country.code}
|
||||
/>
|
||||
<div className="text-base">{country.name}</div>
|
||||
</div>
|
||||
<span className="font-icon scale-150 pointer-events-none">
|
||||
arrow_right
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ function NetworkModeSelect() {
|
||||
</RadioGroup.Label>
|
||||
<RadioGroup.Description
|
||||
as="span"
|
||||
className="text-base text-ciment-feet dark:text-mercury-mist"
|
||||
className="text-base text-cement-feet dark:text-mercury-mist"
|
||||
>
|
||||
<span>{mode.desc}</span>
|
||||
</RadioGroup.Description>
|
||||
|
||||
@@ -12,14 +12,14 @@ export default function CountryList({
|
||||
}: CountryListProps) {
|
||||
const { t } = useTranslation('nodeLocation');
|
||||
return (
|
||||
<ul className="flex flex-col w-full items-stretch px-6 gap-4">
|
||||
<ul className="flex flex-col w-full items-stretch gap-4">
|
||||
{countries && countries.length > 0 ? (
|
||||
countries.map((country) => (
|
||||
<li key={country.code} className="list-none w-full">
|
||||
<div
|
||||
role="presentation"
|
||||
onKeyDown={() => onClick(country.name, country.code)}
|
||||
className="flex flex-row justify-between dark:hover:bg-baltic-sea-jaguar hover:bg-coal-mine-light rounded-lg cursor-pointer"
|
||||
className="flex flex-row justify-between dark:hover:bg-gun-powder hover:dark:bg-opacity-80 hover:bg-cement-feet hover:bg-opacity-20 rounded-lg cursor-pointer px-3"
|
||||
onClick={() => onClick(country.name, country.code)}
|
||||
>
|
||||
<div className="flex flex-row items-center m-1 gap-3 p-1 cursor-pointer">
|
||||
@@ -32,7 +32,7 @@ export default function CountryList({
|
||||
{country.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex items-center text-mercury-mist text-xs cursor-pointer">
|
||||
<div className="p-4 flex items-center text-mercury-mist text-sm cursor-pointer">
|
||||
{isSelected(country.code) ? t('selected') : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,22 +66,20 @@ function NodeLocation({ node }: { node: NodeHop }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="h-70 flex flex-col justify-center items-center gap-y-2 p-1">
|
||||
<QuickConnect onClick={handleCountrySelection} />
|
||||
<SearchBox
|
||||
value={search}
|
||||
onChange={filter}
|
||||
placeholder={t('search-country')}
|
||||
/>
|
||||
<span className="mt-3" />
|
||||
<CountryList
|
||||
countries={foundCountries}
|
||||
onClick={handleCountrySelection}
|
||||
isSelected={isCountrySelected}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full flex flex-col p-4">
|
||||
<div className="h-70 flex flex-col justify-center items-center gap-y-2">
|
||||
<QuickConnect onClick={handleCountrySelection} />
|
||||
<SearchBox
|
||||
value={search}
|
||||
onChange={filter}
|
||||
placeholder={t('search-country')}
|
||||
/>
|
||||
<span className="mt-3" />
|
||||
<CountryList
|
||||
countries={foundCountries}
|
||||
onClick={handleCountrySelection}
|
||||
isSelected={isCountrySelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,12 +14,12 @@ export default function QuickConnect({ onClick }: QuickConnectProps) {
|
||||
onKeyDown={() =>
|
||||
onClick(defaultNodeLocation.name, defaultNodeLocation.code)
|
||||
}
|
||||
className="flex px-5 flex-row w-full py-8 cursor-pointer"
|
||||
className="flex px-1 flex-row items-center w-full py-8 cursor-pointer"
|
||||
onClick={() =>
|
||||
onClick(defaultNodeLocation.name, defaultNodeLocation.code)
|
||||
}
|
||||
>
|
||||
<span className="font-icon px-4 cursor-pointer">bolt</span>
|
||||
<span className="font-icon text-2xl px-4 cursor-pointer">bolt</span>
|
||||
<div className="cursor-pointer">{`${QuickConnectPrefix} ${defaultNodeLocation.name}`}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import { InputEvent } from '../../types';
|
||||
|
||||
interface SearchProps {
|
||||
@@ -12,24 +13,30 @@ export default function SearchBox({
|
||||
placeholder,
|
||||
}: SearchProps) {
|
||||
return (
|
||||
<div className="relative w-full flex flex-row justify-center px-6">
|
||||
<div className="w-full relative flex flex-row items-center px-4 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
id="floating_outlined"
|
||||
id="country_search"
|
||||
value={value}
|
||||
className="dark:bg-baltic-sea pl-9 dark:placeholder-white border border-gun-powder block px-2.5 pb-4 pt-4 w-full text-sm text-gray-900 bg-transparent rounded-lg border-1 border-gray-300 appearance-none dark:text-white dark:border-gray-600 focus:outline-none focus:ring-0 peer"
|
||||
className={clsx([
|
||||
'bg-blanc-nacre dark:bg-baltic-sea focus:outline-none focus:ring-0',
|
||||
'w-full flex flex-row justify-between items-center py-3 px-4 pl-11',
|
||||
'text-baltic-sea dark:text-mercury-pinkish',
|
||||
'placeholder:text-cement-feet placeholder:dark:text-mercury-mist',
|
||||
'border-cement-feet dark:border-gun-powder border-2 rounded-lg',
|
||||
'relative text-base',
|
||||
])}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<span className="font-icon scale-125 pointer-events-none absolute fill-current top-1/2 transform -translate-y-1/2 left-9">
|
||||
<span
|
||||
className={clsx([
|
||||
'font-icon text-2xl absolute left-7',
|
||||
'text-baltic-sea dark:text-laughing-jack',
|
||||
])}
|
||||
>
|
||||
search
|
||||
</span>
|
||||
<label
|
||||
htmlFor="floating_outlined"
|
||||
className="dark:text-white dark:bg-baltic-sea absolute text-sm text-gray-500 dark:text-gray-400 ml-8 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-blanc-nacre dark:bg-gray-900 px-2 peer-placeholder-shown:px-2 peer-placeholder-shown:top-2 peer-placeholder-shown:scale-75 peer-placeholder-shown:-translate-y-4 rtl:peer-placeholder-shown:translate-x-1/4 rtl:peer-placeholder-shown:left-auto start-1"
|
||||
>
|
||||
Search
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ function Settings() {
|
||||
<span
|
||||
className={clsx([
|
||||
darkModeEnabled ? 'translate-x-6' : 'translate-x-1',
|
||||
'inline-block h-4 w-4 transform rounded-full bg-ciment-feet dark:bg-white transition',
|
||||
'inline-block h-4 w-4 transform rounded-full bg-cement-feet dark:bg-white transition',
|
||||
])}
|
||||
/>
|
||||
</Switch>
|
||||
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
'dim-gray': '#696571',
|
||||
// [L] network mode desc text
|
||||
// [L] node location select outline
|
||||
'ciment-feet': '#79747E',
|
||||
'cement-feet': '#79747E',
|
||||
// [D] node location select outline
|
||||
'gun-powder': '#49454F',
|
||||
// [D] top-bar icon
|
||||
|
||||