Add pill view
This commit is contained in:
@@ -9,11 +9,13 @@ export type Settings = {
|
||||
viewAsTable: boolean;
|
||||
useSavedBackgrounds: boolean;
|
||||
unsplashQuery: string;
|
||||
viewMode: 'tile' | 'pill';
|
||||
};
|
||||
|
||||
export const settingsAtom = atomWithStorage<Settings>('vertex-settings', {
|
||||
showLocations: false,
|
||||
showImageDetails: false,
|
||||
viewMode: 'tile',
|
||||
viewAsTable: false,
|
||||
useSavedBackgrounds: false,
|
||||
unsplashQuery: '',
|
||||
|
||||
@@ -20,7 +20,12 @@
|
||||
|
||||
.AppList {
|
||||
display: flex;
|
||||
gap: clamp(.5rem, 1vw, 2rem);
|
||||
gap: clamp(0.5rem, 1vw, 1rem) clamp(0.5rem, 1vw, 2rem);
|
||||
margin: 0 auto 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.AppList.Pill {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { AppDefinition } from '~/types';
|
||||
import { AppButton } from './app-button';
|
||||
|
||||
import style from './app-list.module.css';
|
||||
import { AppPill } from './app-pill';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { settingsAtom } from '~/atoms';
|
||||
|
||||
type AppListProps = {
|
||||
category: string;
|
||||
@@ -9,17 +12,23 @@ type AppListProps = {
|
||||
onReset?: () => void;
|
||||
};
|
||||
export function AppList({ category, apps, onReset }: AppListProps) {
|
||||
const { viewMode } = useAtomValue(settingsAtom);
|
||||
|
||||
const Component = viewMode === 'tile' ? AppButton : AppPill;
|
||||
|
||||
return (
|
||||
<div className={style.AppListWrap}>
|
||||
<h2 className={style.Header}>
|
||||
{category}
|
||||
<span className={style.Close} onClick={onReset}>
|
||||
×
|
||||
</span>
|
||||
</h2>
|
||||
<div className={style.AppList}>
|
||||
{category && (
|
||||
<h2 className={style.Header}>
|
||||
{category}
|
||||
<span className={style.Close} onClick={onReset}>
|
||||
×
|
||||
</span>
|
||||
</h2>
|
||||
)}
|
||||
<div className={`${style.AppList} ${viewMode === 'tile' ? style.Tile : style.Pill}`}>
|
||||
{apps.length > 0
|
||||
? apps.map((app) => <AppButton key={app.url} {...app} />)
|
||||
? apps.map((app) => <Component key={app.url} {...app} />)
|
||||
: 'No apps found'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
66
src/components/apps/app-pill.module.css
Normal file
66
src/components/apps/app-pill.module.css
Normal file
@@ -0,0 +1,66 @@
|
||||
.App {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
backdrop-filter: var(--bg-blur);
|
||||
color: var(--color-text);
|
||||
border-radius: var(--border-radius-default);
|
||||
cursor: pointer;
|
||||
|
||||
background-color: var(--color-bg-dark);
|
||||
transition: background-color var(--transition-duration-default) var(--transition-fn);
|
||||
}
|
||||
|
||||
.App:hover,
|
||||
.Box:focus-within {
|
||||
background-color: var(--color-bg-darker);
|
||||
}
|
||||
|
||||
.App > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-text);
|
||||
gap: 0.5rem;
|
||||
padding: 0.8rem 0.7rem;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.Icon {
|
||||
height: 30px;
|
||||
aspect-ratio: 1/1;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.Icon.Bordered {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.Url {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.Badge {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.Text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.Name {
|
||||
line-height: 1.3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.Index {
|
||||
font-size: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-family: var(--font-accent);
|
||||
}
|
||||
44
src/components/apps/app-pill.tsx
Normal file
44
src/components/apps/app-pill.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { getAppIcon } from '~/api/get-app-icon';
|
||||
import { settingsAtom } from '~/atoms';
|
||||
import { AppDefinition } from '~/types';
|
||||
import style from './app-pill.module.css';
|
||||
import { Anchor, Badge, Flex } from '@mantine/core';
|
||||
|
||||
export type AppPillProps = AppDefinition;
|
||||
|
||||
export function AppPill({
|
||||
name,
|
||||
url,
|
||||
location,
|
||||
icon,
|
||||
icon_source,
|
||||
icon_border,
|
||||
shortcut,
|
||||
}: AppPillProps) {
|
||||
const { showLocations } = useAtomValue(settingsAtom);
|
||||
|
||||
return (
|
||||
<div className={style.App}>
|
||||
<Anchor href={url} target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
className={`${style.Icon} ${icon_border ? style.Bordered : ''} `}
|
||||
src={getAppIcon(icon, icon_source)}
|
||||
alt={name}
|
||||
/>
|
||||
<div className={style.Text}>
|
||||
<span className={style.Name}>{name}</span>
|
||||
<span className={style.Url}>{url.split('://')[1].split('/')[0]}</span>
|
||||
</div>
|
||||
<Flex align="center" gap={4} justify="flex-end" style={{ marginLeft: 'auto' }}>
|
||||
{shortcut && <div className={style.Index}>{shortcut}</div>}
|
||||
{showLocations && location && (
|
||||
<Badge size="xs" color="gray.8" className={style.Badge}>
|
||||
{location}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
</Anchor>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,16 @@
|
||||
import { Badge, Button, Checkbox, Flex, Modal, TextInput, Tooltip } from '@mantine/core';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Flex,
|
||||
Modal,
|
||||
SegmentedControl,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconSettings, IconTrash } from '@tabler/icons-react';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
@@ -20,15 +32,32 @@ export function Settings(props: SettingsProps) {
|
||||
<IconSettings onClick={open} size={24} />
|
||||
</Tooltip>
|
||||
|
||||
<Modal centered radius="lg" title="Settings" opened={isOpen} onClose={close}>
|
||||
<Modal
|
||||
centered
|
||||
radius="lg"
|
||||
title={<Title order={3}>Settings</Title>}
|
||||
size="lg"
|
||||
padding="lg"
|
||||
opened={isOpen}
|
||||
onClose={close}
|
||||
>
|
||||
<Flex direction="column" gap="1rem">
|
||||
<Checkbox
|
||||
label="Show image details"
|
||||
checked={appSettings.showImageDetails}
|
||||
onChange={(e) =>
|
||||
setAppSettings((prev) => ({ ...prev, showImageDetails: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
<Title order={5}>App List</Title>
|
||||
<Flex align="center" gap="sm">
|
||||
<Text size="sm">App View</Text>
|
||||
<SegmentedControl
|
||||
fullWidth={false}
|
||||
w="30%"
|
||||
value={appSettings.viewMode}
|
||||
onChange={(value) =>
|
||||
setAppSettings((prev) => ({ ...prev, viewMode: value as 'tile' | 'pill' }))
|
||||
}
|
||||
data={[
|
||||
{ label: 'Tiles', value: 'tile' },
|
||||
{ label: 'Pills', value: 'pill' },
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
<Checkbox
|
||||
label="Show locations"
|
||||
checked={appSettings.showLocations}
|
||||
@@ -36,10 +65,23 @@ export function Settings(props: SettingsProps) {
|
||||
setAppSettings((prev) => ({ ...prev, showLocations: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<Title order={5}>Backgrounds</Title>
|
||||
<Checkbox
|
||||
label="Show image details"
|
||||
checked={appSettings.showImageDetails}
|
||||
onChange={(e) =>
|
||||
setAppSettings((prev) => ({ ...prev, showImageDetails: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
label={
|
||||
<>
|
||||
Use favorited backgrounds <Badge size="sm">{favoriteBackgrounds.length}</Badge>
|
||||
Use favorited backgrounds{' '}
|
||||
<Badge size="sm" color="gray.7">
|
||||
{favoriteBackgrounds.length}
|
||||
</Badge>
|
||||
</>
|
||||
}
|
||||
checked={appSettings.useSavedBackgrounds}
|
||||
@@ -50,13 +92,15 @@ export function Settings(props: SettingsProps) {
|
||||
|
||||
<TextInput
|
||||
label="Unsplash query"
|
||||
labelProps={{ style: { marginBottom: '4px' } }}
|
||||
value={appSettings.unsplashQuery}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setAppSettings((prev) => ({ ...prev, unsplashQuery: e.target.value }))}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
leftSection={<IconTrash />}
|
||||
color="red.9"
|
||||
leftSection={<IconTrash size={16} />}
|
||||
onClick={() => {
|
||||
localStorage.removeItem('start-page-background');
|
||||
window.location.reload();
|
||||
|
||||
@@ -36,13 +36,14 @@ export function WeatherWidget() {
|
||||
<div className={`${style.WeatherLine} ${style.Delimiter}`}>
|
||||
<i className={`wi wi-owm-${weather.id} ${style.WeatherIcon}`}></i>
|
||||
<div className={style.FeelsLike}>
|
||||
<span>{weather.feels_like}°</span>
|
||||
<span>{Math.round(weather.feels_like)}°</span>
|
||||
<span>{weather.main}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.WeatherLine}>
|
||||
<IconChevronUp size={16} color="lightcoral" />
|
||||
{weather.temp_min}° <IconChevronDown size={16} color="lightgreen" /> {weather.temp_max}
|
||||
{Math.round(weather.temp_min)}° <IconChevronDown size={16} color="lightgreen" />{' '}
|
||||
{Math.round(weather.temp_max)}
|
||||
°
|
||||
<IconDroplet size={14} />
|
||||
{weather.humidity}%<br />
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
padding: 0 clamp(1rem, 10vw, 10rem);
|
||||
padding: 0 clamp(0.6rem, 5vw, 10rem);
|
||||
}
|
||||
|
||||
.Widgets {
|
||||
@@ -23,7 +23,7 @@
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
margin-top: calc(50vh - 240px);
|
||||
margin-top: calc(20vh - 240px);
|
||||
}
|
||||
|
||||
.MainWidgets {
|
||||
|
||||
@@ -139,7 +139,10 @@ export function IndexPage() {
|
||||
onReset={() => setSearchTerm('')}
|
||||
/>
|
||||
) : (
|
||||
<WeatherWidget />
|
||||
<>
|
||||
<WeatherWidget />
|
||||
<AppList category="" apps={apps} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user