diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 36f4dbf..02910c1 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -2708,11 +2708,6 @@
"error-ex": "^1.2.0"
}
},
- "parse-ms": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
- "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="
- },
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
@@ -2812,14 +2807,6 @@
"fast-diff": "^1.1.2"
}
},
- "pretty-ms": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
- "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==",
- "requires": {
- "parse-ms": "^2.1.0"
- }
- },
"progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 96c6cfa..57969bb 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -24,7 +24,6 @@
"overlayscrollbars": "^1.13.1",
"overlayscrollbars-react": "^0.2.3",
"postcss-import": "^14.0.2",
- "pretty-ms": "^7.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^11.12.0",
diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json
index 0197d46..4eab82c 100644
--- a/frontend/src/locale/en/translation.json
+++ b/frontend/src/locale/en/translation.json
@@ -125,6 +125,31 @@
"streamer": "Streamer only"
},
"remove-command-title": "Remove command {{name}}?"
+ },
+ "bottimers": {
+ "title": "Bot timers",
+ "desc": "Define reminders such as checking out your social media or ongoing events",
+ "add-button": "New timer",
+ "search-placeholder": "Search timer by name",
+ "timer-header-new": "New timer",
+ "timer-header-edit": "Edit timer",
+ "timer-name": "Timer name",
+ "timer-name-placeholder": "my-timer",
+ "timer-action-new": "Create",
+ "timer-action-edit": "Edit",
+ "remove-timer-title": "Remove timer {{name}}?",
+ "timer-parameters": "every {{time}}, ≥ {{messages}} messages in the last {{interval}}",
+ "timer-interval": "Minimul interval",
+ "timer-activity": "Minimul chat activity (0 to disable)",
+ "timer-activity-desc": "messages in the last 5 minutes",
+ "timer-messages": "Messages"
+ },
+ "auth": {
+ "title": "Authentication required",
+ "desc": "The installation of strimertül you are trying to reach is protected with a password. Please write the password below to access the control panel.",
+ "no-pwd-note": " If the database has no password (for example, it was recently changed from having one to none), leave the field empty.",
+ "password": "Password",
+ "submit": "Authenticate"
}
},
"form-actions": {
@@ -137,9 +162,18 @@
"disable": "Disable",
"delete": "Delete",
"cancel": "Cancel",
- "ok": "OK"
+ "ok": "OK",
+ "add": "Add"
},
"debug": {
"dev-build": "Development build"
+ },
+ "time": {
+ "x-hours": "{{time}} hrs",
+ "x-minutes": "{{time}} min",
+ "x-seconds": "{{time}} sec",
+ "hours": "hours",
+ "minutes": "minutes",
+ "seconds": "seconds"
}
}
diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx
index f7607c4..14da8ab 100644
--- a/frontend/src/ui/App.tsx
+++ b/frontend/src/ui/App.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
ChatBubbleIcon,
@@ -20,13 +20,27 @@ import ServerSettingsPage from './pages/ServerSettings';
import { RootState } from '../store';
import { createWSClient } from '../store/api/reducer';
import { ConnectionStatus } from '../store/api/types';
-import { styled } from './theme';
+import {
+ Button,
+ Dialog,
+ DialogContainer,
+ FlexRow,
+ InputBox,
+ PageHeader,
+ PageTitle,
+ styled,
+ TextBlock,
+} from './theme';
// @ts-expect-error Asset import
import spinner from '../assets/icon-loading.svg';
import BackendIntegrationPage from './pages/BackendIntegration';
import TwitchSettingsPage from './pages/TwitchSettings';
import TwitchBotCommandsPage from './pages/BotCommands';
+import TwitchBotTimersPage from './pages/BotTimers';
+import { useTranslation } from 'react-i18next';
+import DialogContent from './components/DialogContent';
+import AuthDialog from './pages/AuthDialog';
const LoadingDiv = styled('div', {
display: 'flex',
@@ -46,18 +60,6 @@ function Loading() {
);
}
-
-function AuthDialog() {
- const AuthWrapper = styled('div', {
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- minHeight: '100vh',
- });
-
- return ;
-}
-
const sections: RouteSection[] = [
{
title: 'menu.sections.monitor',
@@ -194,6 +196,10 @@ export default function App(): JSX.Element {
path="/twitch/bot/commands"
element={}
/>
+ }
+ />
diff --git a/frontend/src/ui/components/Interval.tsx b/frontend/src/ui/components/Interval.tsx
new file mode 100644
index 0000000..867f326
--- /dev/null
+++ b/frontend/src/ui/components/Interval.tsx
@@ -0,0 +1,88 @@
+import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { getInterval } from '../../lib/time-utils';
+import { ComboBox, FlexRow, InputBox } from '../theme';
+
+export interface TimeUnit {
+ multiplier: number;
+ unit: string;
+}
+
+export const seconds = { multiplier: 1, unit: 'time.seconds' };
+export const minutes = { multiplier: 60, unit: 'time.minutes' };
+export const hours = { multiplier: 3600, unit: 'time.hours' };
+
+export interface IntervalProps {
+ active: boolean;
+ value: number;
+ id?: string;
+ min?: number;
+ units?: TimeUnit[];
+ onChange?: (value: number) => void;
+}
+
+function Interval({ id, active, value, min, units, onChange }: IntervalProps) {
+ const { t } = useTranslation();
+
+ const timeUnits = units ?? [seconds, minutes, hours];
+
+ const [numInitialValue, multInitialValue] = getInterval(value);
+ const [num, setNum] = useState(numInitialValue);
+ const [mult, setMult] = useState(multInitialValue);
+
+ useEffect(() => {
+ const total = num * mult;
+ if (min && total < min) {
+ const [minNum, minMult] = getInterval(min);
+ setNum(minNum);
+ setMult(minMult);
+ }
+ onChange(Math.max(min ?? 0, total));
+ }, [num, mult]);
+
+ return (
+ <>
+
+ {
+ const intNum = parseInt(ev.target.value, 10);
+ if (Number.isNaN(intNum)) {
+ return;
+ }
+ setNum(intNum);
+ }}
+ placeholder="#"
+ />
+ {
+ const intMult = parseInt(ev.target.value, 10);
+ if (Number.isNaN(intMult)) {
+ return;
+ }
+ setMult(intMult);
+ }}
+ >
+ {timeUnits.map((unit) => (
+
+ ))}
+
+
+ >
+ );
+}
+
+export default React.memo(Interval);
diff --git a/frontend/src/ui/components/MultiInput.tsx b/frontend/src/ui/components/MultiInput.tsx
new file mode 100644
index 0000000..b1c5577
--- /dev/null
+++ b/frontend/src/ui/components/MultiInput.tsx
@@ -0,0 +1,78 @@
+import { Cross2Icon } from '@radix-ui/react-icons';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Button, FlexRow, InputBox, Textarea } from '../theme';
+
+export interface MessageArrayProps {
+ placeholder?: string;
+ value: string[];
+ onChange: (value: string[]) => void;
+}
+
+function MessageArray({ value, placeholder, onChange }: MessageArrayProps) {
+ const { t } = useTranslation();
+
+ return (
+ <>
+ {value.map((message, index) => (
+
+
+
+ {value.length > 1 && (
+
+ )}
+
+
+ ))}
+
+
+
+ >
+ );
+}
+
+export default React.memo(MessageArray);
diff --git a/frontend/src/ui/components/Sidebar.tsx b/frontend/src/ui/components/Sidebar.tsx
index 287187d..142f945 100644
--- a/frontend/src/ui/components/Sidebar.tsx
+++ b/frontend/src/ui/components/Sidebar.tsx
@@ -38,10 +38,12 @@ const Header = styled('div', {
const AppName = styled('h1', {
display: 'flex',
alignItems: 'center',
+ justifyContent: 'center',
gap: '0.2rem',
fontSize: '1.4rem',
margin: '0.5rem 0 0.5rem 0',
fontWeight: 300,
+ paddingRight: '0.5rem',
});
const VersionLabel = styled('div', {
@@ -49,7 +51,7 @@ const VersionLabel = styled('div', {
fontSize: '0.75rem',
fontWeight: 'bold',
color: '$teal8',
- paddingLeft: '12px',
+ textAlign: 'center',
});
const UpdateButton = styled('a', {
diff --git a/frontend/src/ui/pages/AuthDialog.tsx b/frontend/src/ui/pages/AuthDialog.tsx
new file mode 100644
index 0000000..e55bb72
--- /dev/null
+++ b/frontend/src/ui/pages/AuthDialog.tsx
@@ -0,0 +1,80 @@
+import { styled } from '@stitches/react';
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Button, InputBox, TextBlock } from '../theme';
+
+const AuthWrapper = styled('div', {
+ alignItems: 'center',
+ minHeight: '100vh',
+ maxWidth: '600px',
+ display: 'flex',
+ justifyContent: 'center',
+ width: '90vw',
+ margin: '0 auto',
+});
+const AuthTitle = styled('div', {
+ fontWeight: 'bold',
+ color: '$teal12',
+ fontSize: '15pt',
+ borderBottom: '1px solid $teal6',
+ padding: '1rem 1.5rem',
+ margin: '0',
+ lineHeight: '1.25',
+});
+const AuthDialogContainer = styled('form', {
+ display: 'flex',
+ flexDirection: 'column',
+ padding: '0',
+ backgroundColor: '$gray2',
+ borderRadius: '0.25rem',
+ boxShadow:
+ 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
+});
+const Content = styled('div', {
+ display: 'flex',
+ flexDirection: 'column',
+ padding: '1.5rem',
+ gap: '1rem',
+});
+const Actions = styled('div', {
+ display: 'flex',
+ gap: '0.5rem',
+ justifyContent: 'flex-end',
+ borderTop: '1px solid $gray6',
+ padding: '1rem 1.5rem',
+});
+
+export default function AuthDialog(): React.ReactElement {
+ const [password, setPassword] = useState('');
+ const { t } = useTranslation();
+
+ return (
+
+ {
+ e.preventDefault();
+ localStorage.setItem('password', password);
+ window.location.reload();
+ }}
+ >
+ {t('pages.auth.title')}
+
+ {t('pages.auth.desc')}
+ {t('pages.auth.no-pwd-note')}
+
+
+ setPassword(e.target.value)}
+ css={{ flex: 1 }}
+ placeholder={t('pages.auth.password')}
+ />
+
+
+
+
+ );
+}
diff --git a/frontend/src/ui/pages/BotCommands.tsx b/frontend/src/ui/pages/BotCommands.tsx
index f3aafc1..9f61bae 100644
--- a/frontend/src/ui/pages/BotCommands.tsx
+++ b/frontend/src/ui/pages/BotCommands.tsx
@@ -22,6 +22,7 @@ import {
FlexRow,
InputBox,
Label,
+ MultiButton,
PageContainer,
PageHeader,
PageTitle,
@@ -103,7 +104,7 @@ interface CommandItemProps {
onDelete?: () => void;
}
-function CommandItemEl({
+function CommandItem({
name,
item,
onToggle,
@@ -126,36 +127,38 @@ function CommandItemEl({
{item.access_level !== 'streamer' && '+'}
)}
-
-
-
-
-
-
- (onDelete ? onDelete() : null)}
- />
-
+
+
+
+
+
+
+
+ (onDelete ? onDelete() : null)}
+ />
+
+
{item.response}
@@ -163,8 +166,6 @@ function CommandItemEl({
);
}
-const CommandItem = React.memo(CommandItemEl);
-
type DialogPrompt =
| { kind: 'new' }
| { kind: 'edit'; name: string; item: TwitchBotCustomCommand };
@@ -269,20 +270,20 @@ function CommandDialog({
}
export default function TwitchBotCommandsPage(): React.ReactElement {
- const [botCommands, setBotCommands] = useModule(modules.twitchBotCommands);
- const [commandFilter, setCommandFilter] = useState('');
+ const [commands, setCommands] = useModule(modules.twitchBotCommands);
+ const [filter, setFilter] = useState('');
const [activeDialog, setActiveDialog] = useState(null);
const { t } = useTranslation();
const dispatch = useDispatch();
- const commandFilterLC = commandFilter.toLowerCase();
+ const filterLC = filter.toLowerCase();
const setCommand = (newName: string, data: TwitchBotCustomCommand): void => {
switch (activeDialog.kind) {
case 'new':
dispatch(
- setBotCommands({
- ...botCommands,
+ setCommands({
+ ...commands,
[newName]: {
...data,
enabled: true,
@@ -293,8 +294,8 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
case 'edit': {
const oldName = activeDialog.name;
dispatch(
- setBotCommands({
- ...botCommands,
+ setCommands({
+ ...commands,
[oldName]: undefined,
[newName]: data,
}),
@@ -307,8 +308,8 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
const deleteCommand = (cmd: string): void => {
dispatch(
- setBotCommands({
- ...botCommands,
+ setCommands({
+ ...commands,
[cmd]: undefined,
}),
);
@@ -316,11 +317,11 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
const toggleCommand = (cmd: string): void => {
dispatch(
- setBotCommands({
- ...botCommands,
+ setCommands({
+ ...commands,
[cmd]: {
- ...botCommands[cmd],
- enabled: !botCommands[cmd].enabled,
+ ...commands[cmd],
+ enabled: !commands[cmd].enabled,
},
}),
);
@@ -344,25 +345,25 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
setCommandFilter(e.target.value)}
+ value={filter}
+ onChange={(e) => setFilter(e.target.value)}
/>
- {Object.keys(botCommands ?? {})
- ?.filter((cmd) => cmd.toLowerCase().includes(commandFilterLC))
+ {Object.keys(commands ?? {})
+ ?.filter((cmd) => cmd.toLowerCase().includes(filterLC))
.sort()
.map((cmd) => (
toggleCommand(cmd)}
onEdit={() =>
setActiveDialog({
kind: 'edit',
name: cmd,
- item: botCommands[cmd],
+ item: commands[cmd],
})
}
onDelete={() => deleteCommand(cmd)}
diff --git a/frontend/src/ui/pages/BotTimers.tsx b/frontend/src/ui/pages/BotTimers.tsx
new file mode 100644
index 0000000..eb71620
--- /dev/null
+++ b/frontend/src/ui/pages/BotTimers.tsx
@@ -0,0 +1,417 @@
+import { PlusIcon } from '@radix-ui/react-icons';
+import React, { useState } from 'react';
+import { TFunction, useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+import { useModule } from '../../lib/react-utils';
+import { modules } from '../../store/api/reducer';
+import { TwitchBotTimer } from '../../store/api/types';
+import AlertContent from '../components/AlertContent';
+import DialogContent from '../components/DialogContent';
+import Interval, { hours, minutes } from '../components/Interval';
+import MultiInput from '../components/MultiInput';
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogClose,
+ Field,
+ FlexRow,
+ InputBox,
+ Label,
+ MultiButton,
+ PageContainer,
+ PageHeader,
+ PageTitle,
+ styled,
+ TextBlock,
+} from '../theme';
+import { Alert, AlertTrigger } from '../theme/alert';
+
+const TimerList = styled('div', { marginTop: '1rem' });
+const TimerItemContainer = styled('article', {
+ backgroundColor: '$gray2',
+ margin: '0.5rem 0',
+ padding: '0.5rem',
+ borderLeft: '5px solid $teal8',
+ borderRadius: '0.25rem',
+ borderBottom: '1px solid $gray4',
+ transition: 'all 50ms',
+ '&:hover': {
+ backgroundColor: '$gray3',
+ },
+ variants: {
+ status: {
+ enabled: {},
+ disabled: {
+ borderLeftColor: '$red7',
+ backgroundColor: '$gray3',
+ color: '$gray10',
+ },
+ },
+ },
+});
+const TimerHeader = styled('header', {
+ display: 'flex',
+ gap: '0.5rem',
+ alignItems: 'center',
+ marginBottom: '0.4rem',
+});
+const TimerName = styled('span', {
+ color: '$teal10',
+ fontWeight: 'bold',
+ variants: {
+ status: {
+ enabled: {},
+ disabled: {
+ color: '$gray10',
+ },
+ },
+ },
+});
+const TimerDescription = styled('span', {
+ flex: 1,
+});
+const TimerActions = styled('div', {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '0.25rem',
+});
+const TimerText = styled('div', {
+ fontFamily: 'Space Mono',
+ fontSize: '10pt',
+ margin: '0 -0.5rem',
+ marginTop: '0',
+ marginBottom: '0.3rem',
+ padding: '0.5rem',
+ backgroundColor: '$gray4',
+ lineHeight: '1.2rem',
+ '&:last-child': {
+ marginBottom: '-0.5rem',
+ },
+});
+
+function humanTime(t: TFunction<'translation'>, secs: number): string {
+ const mins = Math.floor(secs / 60);
+ const hrs = Math.floor(mins / 60);
+
+ if (hrs > 0) {
+ return t('time.x-hours', { time: hrs });
+ }
+ if (mins > 0) {
+ return t('time.x-minutes', { time: mins });
+ }
+ return t('time.x-seconds', { time: secs });
+}
+
+interface TimerItemProps {
+ name: string;
+ item: TwitchBotTimer;
+ onToggle?: () => void;
+ onEdit?: () => void;
+ onDelete?: () => void;
+}
+
+function TimerItem({
+ name,
+ item,
+ onToggle,
+ onEdit,
+ onDelete,
+}: TimerItemProps): React.ReactElement {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {name}
+
+
+ (
+ {t('pages.bottimers.timer-parameters', {
+ time: humanTime(t, item.minimum_delay),
+ messages: item.minimum_chat_activity,
+ interval: humanTime(t, 300),
+ })}
+ )
+
+
+
+
+
+
+
+
+
+ (onDelete ? onDelete() : null)}
+ />
+
+
+
+
+ {item.messages?.map((message, index) => (
+ {message}
+ ))}
+
+ );
+}
+
+type DialogPrompt =
+ | { kind: 'new' }
+ | { kind: 'edit'; name: string; item: TwitchBotTimer };
+
+function TimerDialog({
+ kind,
+ name,
+ item,
+ onSubmit,
+}: {
+ kind: 'new' | 'edit';
+ name?: string;
+ item?: TwitchBotTimer;
+ onSubmit?: (name: string, item: TwitchBotTimer) => void;
+}) {
+ const [timerName, setName] = useState(name ?? '');
+ const [messages, setMessages] = useState(item?.messages ?? ['']);
+ const [minDelay, setMinDelay] = useState(item?.minimum_delay ?? 300);
+ const [minActivity, setMinActivity] = useState(
+ item?.minimum_chat_activity ?? 5,
+ );
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ );
+}
+
+export default function TwitchBotTimersPage(): React.ReactElement {
+ const [timerConfig, setTimerConfig] = useModule(modules.twitchBotTimers);
+ const [filter, setFilter] = useState('');
+ const [activeDialog, setActiveDialog] = useState(null);
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+
+ const filterLC = filter.toLowerCase();
+
+ const setTimer = (newName: string, data: TwitchBotTimer): void => {
+ switch (activeDialog.kind) {
+ case 'new':
+ dispatch(
+ setTimerConfig({
+ ...timerConfig,
+ timers: {
+ ...timerConfig.timers,
+ [newName]: {
+ ...data,
+ enabled: true,
+ },
+ },
+ }),
+ );
+ break;
+ case 'edit': {
+ const oldName = activeDialog.name;
+ dispatch(
+ setTimerConfig({
+ ...timerConfig,
+ timers: {
+ ...timerConfig.timers,
+ [oldName]: undefined,
+ [newName]: data,
+ },
+ }),
+ );
+ break;
+ }
+ }
+ setActiveDialog(null);
+ };
+
+ const deleteTimer = (cmd: string): void => {
+ dispatch(
+ setTimerConfig({
+ ...timerConfig,
+ timers: {
+ ...timerConfig.timers,
+ [cmd]: undefined,
+ },
+ }),
+ );
+ };
+
+ const toggleTimer = (cmd: string): void => {
+ dispatch(
+ setTimerConfig({
+ ...timerConfig,
+ timers: {
+ ...timerConfig.timers,
+ [cmd]: {
+ ...timerConfig.timers[cmd],
+ enabled: !timerConfig.timers[cmd].enabled,
+ },
+ },
+ }),
+ );
+ };
+
+ return (
+
+
+ {t('pages.bottimers.title')}
+ {t('pages.bottimers.desc')}
+
+
+
+
+
+ setFilter(e.target.value)}
+ />
+
+
+ {Object.keys(timerConfig?.timers ?? {})
+ ?.filter((cmd) => cmd.toLowerCase().includes(filterLC))
+ .sort()
+ .map((cmd) => (
+ toggleTimer(cmd)}
+ onEdit={() =>
+ setActiveDialog({
+ kind: 'edit',
+ name: cmd,
+ item: timerConfig.timers[cmd],
+ })
+ }
+ onDelete={() => deleteTimer(cmd)}
+ />
+ ))}
+
+
+
+
+ );
+}
diff --git a/frontend/src/ui/theme/forms.ts b/frontend/src/ui/theme/forms.ts
index 1070e71..f1004ca 100644
--- a/frontend/src/ui/theme/forms.ts
+++ b/frontend/src/ui/theme/forms.ts
@@ -1,6 +1,7 @@
import * as UnstyledLabel from '@radix-ui/react-label';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { styled } from './theme';
+import { theme } from '.';
export const Field = styled('fieldset', {
all: 'unset',
@@ -45,7 +46,7 @@ export const InputBox = styled('input', {
fontWeight: '300',
border: '1px solid $gray6',
padding: '0.5rem',
- borderRadius: '0.3rem',
+ borderRadius: theme.borderRadius.form,
backgroundColor: '$gray2',
'&:hover': {
borderColor: '$teal7',
@@ -59,6 +60,13 @@ export const InputBox = styled('input', {
borderColor: '$gray5',
color: '$gray8',
},
+ variants: {
+ border: {
+ none: {
+ borderWidth: '0',
+ },
+ },
+ },
});
export const Textarea = styled('textarea', {
@@ -66,7 +74,7 @@ export const Textarea = styled('textarea', {
fontWeight: '300',
border: '1px solid $gray6',
padding: '0.5rem',
- borderRadius: '0.3rem',
+ borderRadius: theme.borderRadius.form,
backgroundColor: '$gray2',
'&:hover': {
borderColor: '$teal7',
@@ -80,6 +88,13 @@ export const Textarea = styled('textarea', {
borderColor: '$gray5',
color: '$gray8',
},
+ variants: {
+ border: {
+ none: {
+ borderWidth: '0',
+ },
+ },
+ },
});
export const ButtonGroup = styled('div', {
@@ -87,12 +102,17 @@ export const ButtonGroup = styled('div', {
gap: '0.5rem',
});
+export const MultiButton = styled('div', {
+ display: 'flex',
+});
+
export const Button = styled('button', {
all: 'unset',
cursor: 'pointer',
+ color: '$gray12',
fontWeight: '300',
padding: '0.5rem 1rem',
- borderRadius: '0.3rem',
+ borderRadius: theme.borderRadius.form,
fontSize: '1.1rem',
border: '1px solid $gray6',
backgroundColor: '$gray4',
@@ -108,13 +128,34 @@ export const Button = styled('button', {
},
transition: 'all 0.2s',
variants: {
+ border: {
+ none: {
+ borderWidth: '0',
+ },
+ },
styling: {
+ form: {
+ padding: '0.65rem',
+ },
link: {
backgroundColor: 'transparent',
border: 'none',
color: '$teal11',
textDecoration: 'underline',
},
+ multi: {
+ borderRadius: '0',
+ margin: '0 -1px',
+ '&:first-child': {
+ borderRadius: `$borderRadius$form 0 0 $borderRadius$form`,
+ },
+ '&:last-child': {
+ borderRadius: `0 $borderRadius$form $borderRadius$form 0`,
+ },
+ '&:hover': {
+ zIndex: '1',
+ },
+ },
},
size: {
small: {
@@ -177,7 +218,7 @@ export const ComboBox = styled('select', {
fontWeight: '300',
border: '1px solid $gray6',
padding: '0.5rem',
- borderRadius: '0.3rem',
+ borderRadius: theme.borderRadius.form,
backgroundColor: '$gray2',
'&:hover': {
borderColor: '$teal7',
@@ -191,6 +232,13 @@ export const ComboBox = styled('select', {
borderColor: '$gray5',
color: '$gray8',
},
+ variants: {
+ border: {
+ none: {
+ borderWidth: '0',
+ },
+ },
+ },
});
export const Checkbox = styled(CheckboxPrimitive.Root, {
diff --git a/frontend/src/ui/theme/pages.ts b/frontend/src/ui/theme/pages.ts
index 7f41d22..03cb26b 100644
--- a/frontend/src/ui/theme/pages.ts
+++ b/frontend/src/ui/theme/pages.ts
@@ -31,4 +31,11 @@ export const SectionHeader = styled('h2', {
export const TextBlock = styled('p', {
lineHeight: '1.5',
+ variants: {
+ spacing: {
+ none: {
+ margin: '0',
+ },
+ },
+ },
});
diff --git a/frontend/src/ui/theme/theme.ts b/frontend/src/ui/theme/theme.ts
index e7e88df..8b642c4 100644
--- a/frontend/src/ui/theme/theme.ts
+++ b/frontend/src/ui/theme/theme.ts
@@ -37,5 +37,8 @@ export const { styled, theme } = createStitches({
...redDark,
...blackA,
},
+ borderRadius: {
+ form: '0.3rem',
+ },
},
});
diff --git a/frontend/src/ui/theme/utils.ts b/frontend/src/ui/theme/utils.ts
index d5090e8..7c2f796 100644
--- a/frontend/src/ui/theme/utils.ts
+++ b/frontend/src/ui/theme/utils.ts
@@ -1,3 +1,4 @@
+import { theme } from '.';
import { styled } from './theme';
export const FlexRow = styled('div', {
@@ -6,6 +7,12 @@ export const FlexRow = styled('div', {
alignItems: 'center',
justifyContent: 'center',
variants: {
+ border: {
+ form: {
+ border: '1px solid $gray6',
+ borderRadius: theme.borderRadius.form,
+ },
+ },
spacing: {
'1': {
gap: '0.5rem',