Hook up theme and clear responses

This commit is contained in:
Gregory Schier
2023-02-24 12:13:30 -08:00
parent 4319ce9a7b
commit 5a9fb5a3a7
10 changed files with 154 additions and 155 deletions

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/tauri';
import Editor from './components/Editor/Editor'; import Editor from './components/Editor/Editor';
import { HStack, VStack } from './components/Stacks'; import { HStack, VStack } from './components/Stacks';
import { DropdownMenuRadio } from './components/Dropdown'; import { Dropdown, DropdownMenuRadio } from './components/Dropdown';
import { WindowDragRegion } from './components/WindowDragRegion'; import { WindowDragRegion } from './components/WindowDragRegion';
import { IconButton } from './components/IconButton'; import { IconButton } from './components/IconButton';
import { Sidebar } from './components/Sidebar'; import { Sidebar } from './components/Sidebar';
@@ -50,25 +50,10 @@ function App() {
return ( return (
<> <>
<div className="grid grid-cols-[auto_1fr] h-full"> <div className="grid grid-cols-[auto_1fr] h-full">
<Sidebar> <Sidebar />
<HStack as={WindowDragRegion} className="pl-24 px-1" items="center" justify="end">
<IconButton icon="archive" />
<DropdownMenuRadio
onValueChange={null}
value={'get'}
items={[
{ label: 'This is a cool one', value: 'get' },
{ label: 'But this one is better', value: 'put' },
{ label: 'This one is just alright', value: 'post' },
]}
>
<IconButton icon="camera" />
</DropdownMenuRadio>
</HStack>
</Sidebar>
<Grid cols={2}> <Grid cols={2}>
<VStack className="w-full"> <VStack className="w-full">
<HStack as={WindowDragRegion} items="center" className="px-3"> <HStack as={WindowDragRegion} items="center" className="pl-3 pr-1.5">
<UrlBar <UrlBar
method={method} method={method}
url={url} url={url}
@@ -77,20 +62,38 @@ function App() {
sendRequest={sendRequest} sendRequest={sendRequest}
/> />
</HStack> </HStack>
<VStack className="pl-3 px-1.5 py-3" space={3}>
<Editor value="" contentType={contentType} />
</VStack>
</VStack> </VStack>
<VStack className="w-full"> <VStack className="w-full">
<HStack as={WindowDragRegion} items="center" className="pl-3 pr-1"> <HStack as={WindowDragRegion} items="center" className="pl-1.5 pr-1">
<div className="my-1 italic text-gray-500 text-sm w-full"> {response && (
{response?.method.toUpperCase()} <div className="my-1 italic text-gray-500 text-sm w-full pointer-events-none">
&nbsp;&bull;&nbsp; {response.method.toUpperCase()}
{response?.status} &nbsp;&bull;&nbsp;
&nbsp;&bull;&nbsp; {response.status}
{response?.elapsed}ms &nbsp;&bull;&nbsp; &nbsp;&bull;&nbsp;
{response?.elapsed2}ms {response.elapsed}ms &nbsp;&bull;&nbsp;
</div> {response.elapsed2}ms
<IconButton icon="gear" className="ml-auto" size="sm" /> </div>
)}
<Dropdown
items={[
{
label: 'Clear Response',
onSelect: () => setResponse(null),
disabled: !response,
},
{
label: 'Other Thing',
},
]}
>
<IconButton icon="gear" className="ml-auto" size="sm" />
</Dropdown>
</HStack> </HStack>
<VStack className="px-3 py-3" space={3}> <VStack className="pr-3 pl-1.5 py-3" space={3}>
{error && <div className="text-white bg-red-500 px-3 py-1 rounded">{error}</div>} {error && <div className="text-white bg-red-500 px-3 py-1 rounded">{error}</div>}
{response !== null && ( {response !== null && (
<> <>

View File

@@ -56,76 +56,26 @@ export function DropdownMenuRadio({
); );
} }
export function Dropdown() { export interface DropdownProps {
const [bookmarksChecked, setBookmarksChecked] = useState(true); children: ReactNode;
const [urlsChecked, setUrlsChecked] = useState(false); items: {
const [person, setPerson] = useState('pedro'); label: string;
onSelect?: () => void;
disabled?: boolean;
}[];
}
export function Dropdown({ children, items }: DropdownProps) {
return ( return (
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger asChild> <DropdownMenuTrigger>{children}</DropdownMenuTrigger>
<Button aria-label="Customise options">
<HamburgerMenuIcon />
</Button>
</DropdownMenu.Trigger>
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem rightSlot={<HotKey>T</HotKey>}>New Tab</DropdownMenuItem> {items.map((item, i) => (
<DropdownMenuItem rightSlot={<HotKey>N</HotKey>}>New Window</DropdownMenuItem> <DropdownMenuItem key={i} onSelect={() => item.onSelect?.()} disabled={item.disabled}>
<DropdownMenuItem disabled rightSlot={<HotKey>N</HotKey>}> {item.label}
New Private Window </DropdownMenuItem>
</DropdownMenuItem> ))}
<DropdownMenu.Sub>
<DropdownMenuSubTrigger rightSlot={<ChevronRightIcon />}>
More Tools
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem rightSlot={<HotKey>S</HotKey>}>Save Page As</DropdownMenuItem>
<DropdownMenuItem>Create Shortcut</DropdownMenuItem>
<DropdownMenuItem>Name Window</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Developer Tools</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenu.Sub>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
checked={bookmarksChecked}
onCheckedChange={(v) => setBookmarksChecked(!!v)}
rightSlot={<HotKey>B</HotKey>}
leftSlot={
<DropdownMenu.ItemIndicator className="DropdownMenuItemIndicator">
<CheckIcon />
</DropdownMenu.ItemIndicator>
}
>
Show Bookmarks
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={urlsChecked}
onCheckedChange={(v) => setUrlsChecked(!!v)}
leftSlot={
<DropdownMenu.ItemIndicator className="DropdownMenuItemIndicator">
<CheckIcon />
</DropdownMenu.ItemIndicator>
}
>
Show Full URLs
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>People</DropdownMenuLabel>
<DropdownMenu.RadioGroup value={person} onValueChange={setPerson}>
<DropdownMenuRadioItem value="pedro">Pedro Duarte</DropdownMenuRadioItem>
<DropdownMenuRadioItem className="DropdownMenuRadioItem" value="colm">
Colm Tuite
</DropdownMenuRadioItem>
</DropdownMenu.RadioGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenuPortal> </DropdownMenuPortal>
</DropdownMenu.Root> </DropdownMenu.Root>
@@ -157,7 +107,7 @@ const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenu.DropdownMenu
<DropdownMenu.Content <DropdownMenu.Content
ref={ref} ref={ref}
align="start" align="start"
className={classnames(className, dropdownMenuClasses, 'mt-1')} className={classnames(className, dropdownMenuClasses, 'm-0.5')}
{...props} {...props}
> >
{children} {children}
@@ -173,12 +123,14 @@ function DropdownMenuItem({
rightSlot, rightSlot,
className, className,
children, children,
disabled,
...props ...props
}: DropdownMenuItemProps) { }: DropdownMenuItemProps) {
return ( return (
<DropdownMenu.Item <DropdownMenu.Item
asChild asChild
className={classnames(className, { 'opacity-30': props.disabled })} disabled={disabled}
className={classnames(className, { 'opacity-30': disabled })}
{...props} {...props}
> >
<ItemInner leftSlot={leftSlot} rightSlot={rightSlot}> <ItemInner leftSlot={leftSlot} rightSlot={rightSlot}>
@@ -311,7 +263,7 @@ const ItemInner = forwardRef<HTMLDivElement, ItemInnerProps>(function ItemInner(
)} )}
{...props} {...props}
> >
<div className="w-7">{leftSlot}</div> {leftSlot && <div className="w-7">{leftSlot}</div>}
<div>{children}</div> <div>{children}</div>
{rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>} {rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
</div> </div>

View File

@@ -3,14 +3,25 @@ import {
CameraIcon, CameraIcon,
GearIcon, GearIcon,
HomeIcon, HomeIcon,
MoonIcon,
PaperPlaneIcon, PaperPlaneIcon,
SunIcon,
TriangleDownIcon, TriangleDownIcon,
UpdateIcon, UpdateIcon,
} from '@radix-ui/react-icons'; } from '@radix-ui/react-icons';
import classnames from 'classnames'; import classnames from 'classnames';
import { NamedExoticComponent } from 'react'; import { NamedExoticComponent } from 'react';
type IconName = 'archive' | 'home' | 'camera' | 'gear' | 'triangle-down' | 'paper-plane' | 'update'; type IconName =
| 'archive'
| 'home'
| 'camera'
| 'gear'
| 'triangle-down'
| 'paper-plane'
| 'update'
| 'sun'
| 'moon';
const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = { const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = {
'paper-plane': PaperPlaneIcon, 'paper-plane': PaperPlaneIcon,
@@ -20,6 +31,8 @@ const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = {
gear: GearIcon, gear: GearIcon,
home: HomeIcon, home: HomeIcon,
update: UpdateIcon, update: UpdateIcon,
sun: SunIcon,
moon: MoonIcon,
}; };
export interface IconProps { export interface IconProps {

View File

@@ -47,7 +47,7 @@ export function Input({
id={id} id={id}
className={classnames( className={classnames(
className, className,
'bg-transparent min-w-0 pl-3 pr-2 w-full focus:outline-none', 'bg-transparent min-w-0 pl-3 pr-2 w-full focus:outline-none text-gray-900',
leftSlot && 'pl-1', leftSlot && 'pl-1',
rightSlot && 'pr-1', rightSlot && 'pr-1',
size === 'md' && 'h-10', size === 'md' && 'h-10',

View File

@@ -1,32 +1,22 @@
import { HTMLAttributes } from 'react'; import React, { HTMLAttributes } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { IconButton } from './IconButton';
import { Button } from './Button';
import useTheme from '../hooks/useTheme';
import { HStack } from './Stacks'; import { HStack } from './Stacks';
import { WindowDragRegion } from './WindowDragRegion'; import { WindowDragRegion } from './WindowDragRegion';
import { IconButton } from './IconButton';
import { DropdownMenuRadio } from './Dropdown';
import { Button } from './Button';
type Props = HTMLAttributes<HTMLDivElement>; type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'>;
export function Sidebar({ className, ...props }: Props) { export function Sidebar({ className, ...props }: Props) {
const { toggleTheme } = useTheme();
return ( return (
<div <div
className={classnames(className, 'w-52 bg-gray-50/40 h-full border-gray-500/10')} className={classnames(className, 'w-52 bg-gray-50/40 h-full border-gray-500/10')}
{...props} {...props}
> >
<HStack as={WindowDragRegion} className="pl-24 px-1" items="center" justify="end"> <HStack as={WindowDragRegion} items="center" className="pr-1" justify="end">
<IconButton icon="archive" /> <IconButton size="sm" icon="sun" onClick={toggleTheme} />
<DropdownMenuRadio
onValueChange={null}
value={'get'}
items={[
{ label: 'This is a cool one', value: 'get' },
{ label: 'But this one is better', value: 'put' },
{ label: 'This one is just alright', value: 'post' },
]}
>
<IconButton icon="camera" />
</DropdownMenuRadio>
</HStack> </HStack>
<ul className="mx-2 py-2"> <ul className="mx-2 py-2">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => ( {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => (

View File

@@ -28,7 +28,7 @@ export function UrlBar({ sendRequest, onMethodChange, method, onUrlChange, url }
}; };
return ( return (
<form onSubmit={handleSendRequest} className="w-full flex items-center"> <form onSubmit={handleSendRequest} className="w-full flex items-center overflow-hidden">
<Input <Input
hideLabel hideLabel
size="sm" size="sm"

14
src-web/hooks/useTheme.ts Normal file
View File

@@ -0,0 +1,14 @@
import { useEffect } from 'react';
import { setTheme, subscribeToPreferredThemeChange, toggleTheme } from '../lib/theme';
export default function useTheme(subscribeToChanges = true): { toggleTheme: () => void } {
useEffect(() => {
if (!subscribeToChanges) return;
const unsub = subscribeToPreferredThemeChange(setTheme);
return unsub;
}, [subscribeToChanges]);
return {
toggleTheme: toggleTheme,
};
}

22
src-web/lib/theme.ts Normal file
View File

@@ -0,0 +1,22 @@
export type Theme = 'dark' | 'light';
export function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme') ?? getPreferredTheme();
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
}
export function setTheme(theme?: Theme) {
document.documentElement.setAttribute('data-theme', theme ?? getPreferredTheme());
}
export function getPreferredTheme(): Theme {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
export function subscribeToPreferredThemeChange(cb: (theme: Theme) => void): () => void {
const listener = (e: MediaQueryListEvent) => cb(e.matches ? 'dark' : 'light');
const m = window.matchMedia('(prefers-color-scheme: dark)');
m.addEventListener('change', listener);
return () => m.removeEventListener('change', listener);
}

View File

@@ -21,8 +21,12 @@ html, body, #root {
overflow: hidden; overflow: hidden;
} }
* {
transition: background-color 150ms ease, border-color 150ms ease;
}
@layer base { @layer base {
:root { :root, [data-theme="light"] {
/* Colors */ /* Colors */
--color-white: 255 100% 100%; --color-white: 255 100% 100%;
--color-background: var(--color-white); --color-background: var(--color-white);
@@ -79,43 +83,41 @@ html, body, #root {
--border-radius-lg: 0.5rem; --border-radius-lg: 0.5rem;
} }
@media (prefers-color-scheme: dark) { [data-theme="dark"] {
:root { --color-white: 255 100% 100%;
--color-white: 255 100% 100%; --color-background: 217 21% 7%;
--color-background: 217 21% 7%;
--color-blue-900: 217 91% 95%; --color-blue-900: 217 91% 95%;
--color-blue-800: 217 91% 88%; --color-blue-800: 217 91% 88%;
--color-blue-700: 217 91% 76%; --color-blue-700: 217 91% 76%;
--color-blue-600: 217 91% 70%; --color-blue-600: 217 91% 70%;
--color-blue-500: 217 91% 65%; --color-blue-500: 217 91% 65%;
--color-blue-400: 217 91% 58%; --color-blue-400: 217 91% 58%;
--color-blue-300: 217 91% 43%; --color-blue-300: 217 91% 43%;
--color-blue-200: 217 91% 30%; --color-blue-200: 217 91% 30%;
--color-blue-100: 217 91% 20%; --color-blue-100: 217 91% 20%;
--color-blue-50: 217 91% 10%; --color-blue-50: 217 91% 10%;
--color-violet-900: 258 90% 95%; --color-violet-900: 258 90% 95%;
--color-violet-800: 258 90% 88%; --color-violet-800: 258 90% 88%;
--color-violet-700: 258 90% 76%; --color-violet-700: 258 90% 76%;
--color-violet-600: 258 90% 70%; --color-violet-600: 258 90% 70%;
--color-violet-500: 258 90% 65%; --color-violet-500: 258 90% 65%;
--color-violet-400: 258 90% 58%; --color-violet-400: 258 90% 58%;
--color-violet-300: 258 90% 43%; --color-violet-300: 258 90% 43%;
--color-violet-200: 258 90% 30%; --color-violet-200: 258 90% 30%;
--color-violet-100: 258 90% 20%; --color-violet-100: 258 90% 20%;
--color-violet-50: 258 90% 10%; --color-violet-50: 258 90% 10%;
--color-gray-900: 217 21% 95%; --color-gray-900: 217 21% 95%;
--color-gray-800: 217 21% 88%; --color-gray-800: 217 21% 88%;
--color-gray-700: 217 21% 76%; --color-gray-700: 217 21% 76%;
--color-gray-600: 217 21% 70%; --color-gray-600: 217 21% 70%;
--color-gray-500: 217 21% 65%; --color-gray-500: 217 21% 65%;
--color-gray-400: 217 21% 58%; --color-gray-400: 217 21% 58%;
--color-gray-300: 217 21% 43%; --color-gray-300: 217 21% 43%;
--color-gray-200: 217 21% 30%; --color-gray-200: 217 21% 30%;
--color-gray-100: 217 21% 25%; --color-gray-100: 217 21% 25%;
--color-gray-50: 217 21% 15%; --color-gray-50: 217 21% 15%;
}
} }
} }

View File

@@ -4,10 +4,13 @@ import App from './App';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import { MotionConfig } from 'framer-motion'; import { MotionConfig } from 'framer-motion';
import init, { greet } from 'hello'; import init, { greet } from 'hello';
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api';
import { setTheme } from './lib/theme';
import './main.css'; import './main.css';
setTheme();
await init(); await init();
greet(); greet();
await invoke('load_db'); await invoke('load_db');