1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-18 01:50:50 +00:00

feat: (incomplete) logging window

This commit is contained in:
Ash Keel 2022-11-24 19:49:25 +01:00
parent 1ec6da850a
commit 17f00c6960
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
11 changed files with 404 additions and 126 deletions

View file

@ -16,7 +16,10 @@
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-icons": "^1.1.1",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-scroll-area": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.1",
"@radix-ui/react-toggle": "^1.0.1",
"@radix-ui/react-toggle-group": "^1.0.1",
"@radix-ui/react-toolbar": "^1.0.1",
"@redux-devtools/extension": "^3.2.3",
"@reduxjs/toolkit": "^1.9.0",
@ -29,15 +32,12 @@
"i18next": "^22.0.6",
"inter-ui": "^3.19.3",
"normalize.css": "^8.0.1",
"overlayscrollbars": "^2.0.1",
"overlayscrollbars-react": "^0.5.0",
"postcss-import": "^15.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.0.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.3",
"react-toastify": "^9.1.1",
"redux-thunk": "^2.4.2",
"sass": "^1.56.1",
"typescript": "^4.9.3",
@ -708,6 +708,14 @@
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
"integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw=="
},
"node_modules/@radix-ui/number": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz",
"integrity": "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
@ -966,6 +974,27 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.2.tgz",
"integrity": "sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/number": "1.0.0",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-direction": "1.0.0",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-use-callback-ref": "1.0.0",
"@radix-ui/react-use-layout-effect": "1.0.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.1.tgz",
@ -1811,14 +1840,6 @@
"node": ">= 6"
}
},
"node_modules/clsx": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -3921,20 +3942,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/overlayscrollbars": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.0.1.tgz",
"integrity": "sha512-tER9iKasFqcJiYdtspHbzlhJJg8kicyj/Oag/GRAK658rDouat2BGFfSFg3AgIw/Yc9CQ78AuX6ieHgg1wQw7Q=="
},
"node_modules/overlayscrollbars-react": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.0.tgz",
"integrity": "sha512-uCNTnkfWW74veoiEv3kSwoLelKt4e8gTNv65D771X3il0x5g5Yo0fUbro7SpQzR9yNgi23cvB2mQHTTdQH96pA==",
"peerDependencies": {
"overlayscrollbars": "^2.0.0",
"react": ">=16.8.0"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -4350,18 +4357,6 @@
}
}
},
"node_modules/react-toastify": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz",
"integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==",
"dependencies": {
"clsx": "^1.1.1"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -5634,6 +5629,14 @@
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
"integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw=="
},
"@radix-ui/number": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz",
"integrity": "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/primitive": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
@ -5831,6 +5834,23 @@
"@radix-ui/react-use-controllable-state": "1.0.0"
}
},
"@radix-ui/react-scroll-area": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.2.tgz",
"integrity": "sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/number": "1.0.0",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-direction": "1.0.0",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-use-callback-ref": "1.0.0",
"@radix-ui/react-use-layout-effect": "1.0.0"
}
},
"@radix-ui/react-separator": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.1.tgz",
@ -6403,11 +6423,6 @@
}
}
},
"clsx": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -7859,17 +7874,6 @@
"word-wrap": "^1.2.3"
}
},
"overlayscrollbars": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.0.1.tgz",
"integrity": "sha512-tER9iKasFqcJiYdtspHbzlhJJg8kicyj/Oag/GRAK658rDouat2BGFfSFg3AgIw/Yc9CQ78AuX6ieHgg1wQw7Q=="
},
"overlayscrollbars-react": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.0.tgz",
"integrity": "sha512-uCNTnkfWW74veoiEv3kSwoLelKt4e8gTNv65D771X3il0x5g5Yo0fUbro7SpQzR9yNgi23cvB2mQHTTdQH96pA==",
"requires": {}
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -8100,14 +8104,6 @@
"tslib": "^2.0.0"
}
},
"react-toastify": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz",
"integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==",
"requires": {
"clsx": "^1.1.1"
}
},
"read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View file

@ -11,7 +11,10 @@
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-icons": "^1.1.1",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-scroll-area": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.1",
"@radix-ui/react-toggle": "^1.0.1",
"@radix-ui/react-toggle-group": "^1.0.1",
"@radix-ui/react-toolbar": "^1.0.1",
"@redux-devtools/extension": "^3.2.3",
"@reduxjs/toolkit": "^1.9.0",
@ -24,15 +27,12 @@
"i18next": "^22.0.6",
"inter-ui": "^3.19.3",
"normalize.css": "^8.0.1",
"overlayscrollbars": "^2.0.1",
"overlayscrollbars-react": "^0.5.0",
"postcss-import": "^15.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.0.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.3",
"react-toastify": "^9.1.1",
"redux-thunk": "^2.4.2",
"sass": "^1.56.1",
"typescript": "^4.9.3",

View file

@ -1 +1 @@
1de5a15d31eda16cf49dfabfe8142773
b120abf6c619728b1d1a33e9dda709c0

View file

@ -6,8 +6,6 @@ import { HashRouter } from 'react-router-dom';
import 'inter-ui/inter.css';
import '@fontsource/space-mono/index.css';
import 'normalize.css/normalize.css';
import 'react-toastify/dist/ReactToastify.css';
import 'overlayscrollbars/overlayscrollbars.css';
import './locale/setup';

View file

@ -286,5 +286,14 @@
"text": "This page is still under construction, apologies for the lackluster view :("
},
"loading": "{{APPNAME}} is starting up, please wait!"
},
"logging": {
"dialog-title": "Application logs",
"levelFilter": "Filter per log severity",
"level": {
"info": "Info",
"warn": "Warning",
"error": "Error"
}
}
}

View file

@ -13,7 +13,6 @@ import { t } from 'i18next';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Route, Routes } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import {
GetKilovoltBind,
@ -43,6 +42,8 @@ import { APPNAME, styled } from './theme';
// @ts-expect-error Asset import
import spinner from '../assets/icon-loading.svg';
import Scrollbar from './components/utils/Scrollbar';
import LogViewer from './components/LogViewer';
const LoadingDiv = styled('div', {
display: 'flex',
@ -218,31 +219,37 @@ export default function App(): JSX.Element {
return (
<Container>
<LogViewer />
<Sidebar sections={sections} />
<PageContent>
<PageWrapper role="main">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/about" element={<StrimertulPage />} />
<Route path="/debug" element={<DebugPage />} />
<Route path="/http" element={<ServerSettingsPage />} />
<Route path="/twitch/settings" element={<TwitchSettingsPage />} />
<Route
path="/twitch/bot/commands"
element={<TwitchBotCommandsPage />}
/>
<Route
path="/twitch/bot/timers"
element={<TwitchBotTimersPage />}
/>
<Route path="/twitch/bot/alerts" element={<ChatAlertsPage />} />
<Route path="/loyalty/settings" element={<LoyaltyConfigPage />} />
<Route path="/loyalty/users" element={<LoyaltyQueuePage />} />
<Route path="/loyalty/rewards" element={<LoyaltyRewardsPage />} />
</Routes>
</PageWrapper>
</PageContent>
<ToastContainer position="bottom-center" autoClose={5000} theme="dark" />
<Scrollbar
vertical={true}
root={{ flex: 1 }}
viewport={{ height: '100vh', flex: '1' }}
>
<PageContent>
<PageWrapper role="main">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/about" element={<StrimertulPage />} />
<Route path="/debug" element={<DebugPage />} />
<Route path="/http" element={<ServerSettingsPage />} />
<Route path="/twitch/settings" element={<TwitchSettingsPage />} />
<Route
path="/twitch/bot/commands"
element={<TwitchBotCommandsPage />}
/>
<Route
path="/twitch/bot/timers"
element={<TwitchBotTimersPage />}
/>
<Route path="/twitch/bot/alerts" element={<ChatAlertsPage />} />
<Route path="/loyalty/settings" element={<LoyaltyConfigPage />} />
<Route path="/loyalty/users" element={<LoyaltyQueuePage />} />
<Route path="/loyalty/rewards" element={<LoyaltyRewardsPage />} />
</Routes>
</PageWrapper>
</PageContent>
</Scrollbar>
</Container>
);
}

View file

@ -0,0 +1,194 @@
import { Cross2Icon } from '@radix-ui/react-icons';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { RootState } from 'src/store';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import {
Dialog,
DialogContainer,
DialogOverlay,
DialogTitle,
IconButton,
MultiToggle,
MultiToggleItem,
styled,
} from '../theme';
const Floating = styled('div', {
position: 'fixed',
top: '6px',
right: '10px',
display: 'flex',
gap: '3px',
zIndex: 10,
});
const LogBubble = styled('div', {
borderRadius: '6px',
minWidth: '10px',
minHeight: '10px',
backgroundColor: '$gray6',
color: '$gray11',
padding: '4px 5px 3px',
lineHeight: '0.7rem',
fontSize: '0.7rem',
cursor: 'pointer',
variants: {
level: {
info: {},
warn: {
backgroundColor: '$yellow6',
color: '$yellow11',
},
error: {
backgroundColor: '$red6',
color: '$red11',
},
},
},
});
const emptyFilter = {
info: false,
warn: false,
error: false,
};
type LogLevel = keyof typeof emptyFilter;
const levels: LogLevel[] = ['info', 'warn', 'error'];
interface LogDialogProps {
initialFilter: LogLevel;
}
const LevelToggle = styled(MultiToggleItem, {
variants: {
level: {
info: {},
warn: {
backgroundColor: '$yellow4',
'&:hover': {
backgroundColor: '$yellow5',
},
"&[data-state='on']": {
backgroundColor: '$yellow8',
},
},
error: {
backgroundColor: '$red4',
'&:hover': {
backgroundColor: '$red5',
},
"&[data-state='on']": {
backgroundColor: '$red8',
},
},
},
},
});
function LogDialog({ initialFilter }: LogDialogProps) {
const logEntries = useSelector((state: RootState) => state.logging.messages);
const [filter, setFilter] = useState({
...emptyFilter,
[initialFilter]: true,
});
const { t } = useTranslation();
const enabled = levels.filter((level) => filter[level]);
const count = logEntries.reduce((acc, entry) => {
if (entry.level in acc) {
acc[entry.level] += 1;
} else {
acc[entry.level] = 1;
}
return acc;
}, {} as Record<string, number>);
return (
<DialogPrimitive.Portal>
<DialogOverlay />
<DialogContainer>
<DialogTitle style={{ display: 'flex', gap: '1rem' }}>
{t('logging.dialog-title')}
<MultiToggle
type="multiple"
aria-label={t(`logging.levelFilter`)}
value={enabled}
onValueChange={(values: LogLevel[]) => {
const newFilter = { ...emptyFilter };
values.forEach((level) => {
newFilter[level] = true;
});
setFilter(newFilter);
}}
>
{levels.map((level) => (
<LevelToggle
key={level}
size="small"
level={level}
value={level}
aria-label={t(`logging.level.${level}`)}
>
{t(`logging.level.${level}`)} ({count[level] ?? 0})
</LevelToggle>
))}
</MultiToggle>
<DialogPrimitive.DialogClose asChild>
<IconButton>
<Cross2Icon />
</IconButton>
</DialogPrimitive.DialogClose>
</DialogTitle>
<p></p>
</DialogContainer>
</DialogPrimitive.Portal>
);
}
function LogViewer() {
const logEntries = useSelector((state: RootState) => state.logging.messages);
const [activeDialog, setActiveDialog] = useState<LogLevel>(null);
const count = logEntries.reduce((acc, entry) => {
if (entry.level in acc) {
acc[entry.level] += 1;
} else {
acc[entry.level] = 1;
}
return acc;
}, {} as Record<string, number>);
return (
<div>
<Floating>
{levels.map((level) =>
level in count && count[level] > 0 ? (
<LogBubble
key={level}
level={level}
onClick={() => setActiveDialog(level)}
>
{count[level]}
</LogBubble>
) : null,
)}
</Floating>
<Dialog
open={!!activeDialog}
onOpenChange={(open) => {
if (!open) {
// Reset dialog status on dialog close
setActiveDialog(null);
}
}}
>
{activeDialog ? <LogDialog initialFilter={activeDialog} /> : null}
</Dialog>
</div>
);
}
export default React.memo(LogViewer);

View file

@ -3,13 +3,14 @@ import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { Link, useMatch, useResolvedPath } from 'react-router-dom';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { RootState } from '../../store';
import { APPNAME, APPREPO } from '../theme';
import BrowserLink from './BrowserLink';
import Scrollbar from './utils/Scrollbar';
// @ts-expect-error Asset import
import logo from '../../assets/icon-logo.svg';
import BrowserLink from './BrowserLink';
export interface RouteSection {
title: string;
@ -38,6 +39,7 @@ const Header = styled('div', {
});
const AppName = styled('h1', {
userSelect: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@ -49,6 +51,7 @@ const AppName = styled('h1', {
});
const AppLink = styled(Link, {
userSelect: 'none',
all: 'unset',
cursor: 'pointer',
color: '$teal12',
@ -69,6 +72,7 @@ const AppLink = styled(Link, {
});
const VersionLabel = styled('div', {
userSelect: 'none',
textTransform: 'uppercase',
fontSize: '0.75rem',
fontWeight: 'bold',
@ -106,8 +110,10 @@ const MenuHeader = styled('header', {
fontWeight: 'bold',
padding: '0.5rem 0 0.5rem 0.8rem',
color: '$teal9',
userSelect: 'none',
});
const MenuLink = styled(Link, {
userSelect: 'none',
color: '$teal13 !important',
display: 'flex',
alignItems: 'center',
@ -199,10 +205,7 @@ export default function Sidebar({
return (
<Container>
<OverlayScrollbarsComponent
style={{ maxHeight: '100vh' }}
options={{ scrollbars: { autoHide: 'scroll' } }}
>
<Scrollbar vertical={true} viewport={{ maxHeight: '100vh' }}>
<Header>
<AppLink to={'/about'} status={matchApp ? 'active' : 'default'}>
<AppName>
@ -230,7 +233,7 @@ export default function Sidebar({
))}
</MenuSection>
))}
</OverlayScrollbarsComponent>
</Scrollbar>
</Container>
);
}

View file

@ -0,0 +1,64 @@
import React from 'react';
import * as ScrollArea from '@radix-ui/react-scroll-area';
import { styled } from '../../theme';
export interface ScrollbarProps {
vertical?: boolean;
horizontal?: boolean;
root?: React.CSSProperties;
viewport?: React.CSSProperties;
}
const StyledScrollbar = styled(ScrollArea.Scrollbar, {
display: 'flex',
userSelect: 'none',
touchAction: 'none',
padding: '2px',
background: '$blackA6',
transition: 'background 160ms ease-out',
'&:hover': {
background: '$blackA8',
},
});
const StyledThumb = styled(ScrollArea.Thumb, {
flex: '1',
background: '$teal6',
borderRadius: '10px',
position: 'relative',
'&:hover': {
background: '$teal8',
},
});
function Scrollbar({
vertical,
horizontal,
root,
viewport,
children,
}: React.PropsWithChildren<ScrollbarProps>): React.ReactElement {
return (
<ScrollArea.Root style={root ?? {}}>
<ScrollArea.Viewport style={viewport ?? {}}>
{children}
</ScrollArea.Viewport>
{vertical ? (
<StyledScrollbar orientation="vertical" style={{ width: '10px' }}>
<StyledThumb />
</StyledScrollbar>
) : null}
{horizontal ? (
<StyledScrollbar
orientation="horizontal"
style={{ flexDirection: 'column', height: '10px' }}
>
<StyledThumb />
</StyledScrollbar>
) : null}
<ScrollArea.Corner />
</ScrollArea.Root>
);
}
export default React.memo(Scrollbar);

View file

@ -1,5 +1,6 @@
import * as UnstyledLabel from '@radix-ui/react-label';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import * as ToggleGroup from '@radix-ui/react-toggle-group';
import { styled } from './theme';
import { theme } from '.';
@ -115,15 +116,15 @@ export const MultiButton = styled('div', {
display: 'flex',
});
export const Button = styled('button', {
const button = {
all: 'unset',
cursor: 'pointer',
userSelect: 'none',
color: '$gray12',
fontWeight: '300',
padding: '0.5rem 1rem',
borderRadius: theme.borderRadius.form,
fontSize: '1.1rem',
padding: '0.5rem 1rem',
border: '1px solid $gray6',
backgroundColor: '$gray4',
display: 'flex',
@ -220,6 +221,37 @@ export const Button = styled('button', {
},
},
},
};
export const MultiToggle = styled(ToggleGroup.Root, {
display: 'inline-flex',
borderRadius: theme.borderRadius.form,
backgroundColor: '$gray4',
});
export const MultiToggleItem = styled(ToggleGroup.Item, {
...button,
borderRadius: 0,
border: 0,
'&:first-child': {
borderTopLeftRadius: theme.borderRadius.form,
borderBottomLeftRadius: theme.borderRadius.form,
},
'&:last-child': {
borderTopRightRadius: theme.borderRadius.form,
borderBottomRightRadius: theme.borderRadius.form,
},
'&:hover': {
...button['&:hover'],
},
"&[data-state='on']": {
...button['&:active'],
background: '$gray8',
},
});
export const Button = styled('button', {
...button,
});
export const ComboBox = styled('select', {

View file

@ -1,25 +0,0 @@
package http
import "sync"
type SafeBool struct {
val bool
mux sync.RWMutex
}
func newSafeBool(val bool) *SafeBool {
return &SafeBool{val: val, mux: sync.RWMutex{}}
}
func (s *SafeBool) Set(val bool) {
s.mux.Lock()
s.val = val
s.mux.Unlock()
}
func (s *SafeBool) Get() bool {
s.mux.RLock()
val := s.val
s.mux.RUnlock()
return val
}