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

Twitch settings done

This commit is contained in:
Ash Keel 2021-12-18 03:25:00 +01:00
parent 5f59763372
commit 9fa27356b8
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
13 changed files with 602 additions and 26 deletions

View file

@ -433,6 +433,55 @@
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-checkbox": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-0.1.4.tgz",
"integrity": "sha512-UtiV0y4CNmcAdCqRaGGPxeET/asO44rfKxtBbMIxAD4BGPfSIe4+kkF0q624S5c7q07HXO0vhOYlSObR3Fj2bg==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "0.1.0",
"@radix-ui/react-compose-refs": "0.1.0",
"@radix-ui/react-context": "0.1.1",
"@radix-ui/react-label": "0.1.4",
"@radix-ui/react-presence": "0.1.1",
"@radix-ui/react-primitive": "0.1.3",
"@radix-ui/react-use-controllable-state": "0.1.0",
"@radix-ui/react-use-previous": "0.1.0",
"@radix-ui/react-use-size": "0.1.0"
},
"dependencies": {
"@radix-ui/react-id": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.1.4.tgz",
"integrity": "sha512-/hq5m/D0ZfJWOS7TLF+G0l08KDRs87LBE46JkAvgKkg1fW4jkucx9At9D9vauIPSbdNmww5kXEp566hMlA8eXA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "0.1.0"
}
},
"@radix-ui/react-label": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-0.1.4.tgz",
"integrity": "sha512-I59IMdUhHixk6cG4D00UN+oFxTpur9cJQSOl+4EfSTJZs+x4PqDpM7p402/gb9sXJqylsUkDMHDdaPzLwPNf7g==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "0.1.0",
"@radix-ui/react-context": "0.1.1",
"@radix-ui/react-id": "0.1.4",
"@radix-ui/react-primitive": "0.1.3"
}
},
"@radix-ui/react-primitive": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.3.tgz",
"integrity": "sha512-fcyADaaAx2jdqEDLsTs6aX50S3L1c9K9CC6XMpJpuXFJCU4n9PGTFDZRtY2gAoXXoRCPIBsklCopSmGb6SsDjQ==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "0.1.2"
}
}
}
},
"@radix-ui/react-collection": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-0.1.3.tgz",
@ -498,6 +547,16 @@
"@radix-ui/react-primitive": "0.1.2"
}
},
"@radix-ui/react-presence": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-0.1.1.tgz",
"integrity": "sha512-LsL+NcWDpFUAYCmXeH02o4pgqcSLpwxP84UIjCtpIKrsPe2vLuhcp79KC/jZJeXz+of2lUpMAxpM+eCpxFZtlg==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "0.1.0",
"@radix-ui/react-use-layout-effect": "0.1.0"
}
},
"@radix-ui/react-primitive": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.2.tgz",
@ -612,6 +671,22 @@
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-use-previous": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-0.1.0.tgz",
"integrity": "sha512-0fxNc33rYnCzDMPSiSnfS8YklnxQo8WqbAQXPAgIaaA1jRu2qFB916PL4qCIW+avcAAqFD38vWhqDqcVmBharA==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-use-size": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-0.1.0.tgz",
"integrity": "sha512-TcZAsR+BYI46w/RbaSFCRACl+Jh6mDqhu6GS2r0iuJpIVrj8atff7qtTjmMmfGtEDNEjhl7DxN3pr1nTS/oruQ==",
"requires": {
"@babel/runtime": "^7.13.10"
}
},
"@reduxjs/toolkit": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.6.2.tgz",
@ -686,14 +761,6 @@
"csstype": "^3.0.2"
}
},
"@types/react-custom-scroll": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@types/react-custom-scroll/-/react-custom-scroll-4.3.2.tgz",
"integrity": "sha512-0gFAkoTihBzYcyoiw68qYIgTeHwPbCLbRMFftsVYlXLLvXx4y519If/+1r7UtOaF7aAXRRJOCYaHZj/HRyfc8Q==",
"requires": {
"@types/react": "*"
}
},
"@types/react-dom": {
"version": "17.0.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.4.tgz",
@ -2598,11 +2665,6 @@
"object-assign": "^4.1.1"
}
},
"react-custom-scroll": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/react-custom-scroll/-/react-custom-scroll-4.3.0.tgz",
"integrity": "sha512-JHNWBAzyw3MgsCNcG6uXJQkCoVPmWxeEW01ABmUz4HsWxw3EQJ2GzrUa5y4ISI85V3q6O4zwCIZAVn10aM3clQ=="
},
"react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",

View file

@ -4,6 +4,7 @@
"dependencies": {
"@billjs/event-emitter": "^1.0.3",
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-checkbox": "^0.1.4",
"@radix-ui/react-icons": "^1.0.3",
"@radix-ui/react-label": "^0.1.3",
"@radix-ui/react-tabs": "^0.1.4",

View file

@ -72,6 +72,33 @@
"auth-button": "Authenticate as Twitch",
"auth-message": "Click the following button to authenticate the back-end with your Twitch account:",
"current-status": "Current status"
},
"twitch-settings": {
"title": "Twitch configuration",
"enable": "Enable Twitch integration",
"apiguide-1": "You will need to create an application, here's how:",
"apiguide-2": "Go to <1>https://dev.twitch.tv/console/apps/create</1>",
"apiguide-3": "Use the following data for the required fields:",
"oauth-redir-uri": "OAuth Redirect URLs",
"apiguide-4": "Once made, create a <1>New Secret</1>, then copy both fields below and save!",
"app-client-id": "App Client ID",
"app-client-secret": "App Client Secret",
"subtitle": "Twitch integration with streams including chat bot and API access. If you stream on Twitch, you definitely want this on.",
"api-subheader": "Application info",
"api-configuration": "API access",
"bot-settings": "Bot settings",
"enable-bot": "Enable Twitch bot",
"bot-channel": "Twitch channel",
"bot-username": "Twitch account username",
"bot-oauth": "Authorization token",
"bot-oauth-note": "You can get this by logging in with the bot account and going here: <1>https://twitchapps.com/tmi/</1>",
"bot-info-header": "Bot account info",
"bot-settings-copy": "A bot can interact with chat messages and provide extra events for the platform (chat events, some notifications) but requires access to a Twitch account. You can use your own or make a new one (if enabled on your main account, you can re-use the same email for your second account!)",
"bot-chat-header": "Chat logging",
"bot-chat-keys": "Enable chat logging",
"bot-chat-history": "How many messages to keep in history",
"bot-chat-history-suffix": "messages",
"bot-chat-history-desc": "Chat logging allows for pages and modules to read messages from chat. It's recommended to keep chat logging enabled with a reasonable history size (eg. 6)"
}
},
"form-actions": {

View file

@ -25,6 +25,7 @@ import { styled } from './theme';
// @ts-expect-error Asset import
import spinner from '../assets/icon-loading.svg';
import BackendIntegrationPage from './pages/BackendIntegration';
import TwitchSettingsPage from './pages/TwitchSettings';
const LoadingDiv = styled('div', {
display: 'flex',
@ -130,16 +131,23 @@ const sections: RouteSection[] = [
];
const Container = styled('div', {
position: 'relative',
display: 'flex',
flexDirection: 'row',
minHeight: '100vh',
overflow: 'hidden',
height: '100vh',
});
const PageContent = styled('main', {
flex: 1,
overflow: 'auto',
});
const PageWrapper = styled('div', {
display: 'flex',
flexDirection: 'row',
flex: 1,
overflow: 'auto',
overflow: 'hidden',
});
export default function App(): JSX.Element {
@ -175,11 +183,14 @@ export default function App(): JSX.Element {
<Container>
<Sidebar sections={sections} />
<PageContent>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/http" element={<ServerSettingsPage />} />
<Route path="/backend" element={<BackendIntegrationPage />} />
</Routes>
<PageWrapper role="main">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/http" element={<ServerSettingsPage />} />
<Route path="/backend" element={<BackendIntegrationPage />} />
<Route path="/twitch/settings" element={<TwitchSettingsPage />} />
</Routes>
</PageWrapper>
</PageContent>
<ToastContainer position="bottom-center" autoClose={5000} theme="dark" />
</Container>

View file

@ -0,0 +1,40 @@
import React from 'react';
import { styled } from '../theme';
const TableContainer = styled('table', {
borderRadius: '3px',
backgroundColor: '$gray2',
padding: '0.3rem',
margin: '0.5rem 0',
});
const Term = styled('th', {
padding: '0.3rem 0.5rem',
textAlign: 'right',
color: '$teal11',
});
const Definition = styled('td', {
padding: '0.3rem 0.5rem',
});
interface DefinitionTableProps {
entries: Record<string, string>;
}
function DefinitionTable({ entries }: DefinitionTableProps) {
return (
<TableContainer>
<tbody>
{Object.entries(entries).map(([key, value]) => (
<tr key={key}>
<Term>{key}</Term>
<Definition>{value}</Definition>
</tr>
))}
</tbody>
</TableContainer>
);
}
export default React.memo(DefinitionTable);

View file

@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { toast } from 'react-toastify';
import * as Tabs from '@radix-ui/react-tabs';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
import { useModule, useStatus } from '../../lib/react-utils';
import Stulbe from '../../lib/stulbe-lib';
@ -20,8 +19,10 @@ import {
SectionHeader,
styled,
TabButton,
TabContainer,
TabContent,
TabList,
TextBlock,
} from '../theme';
import eventsubTests from '../../data/eventsub-tests';
import { RootState } from '../../store';
@ -299,15 +300,15 @@ export default function BackendIntegrationPage(): React.ReactElement {
<PageContainer>
<PageHeader>
<PageTitle>{t('pages.stulbe.title')}</PageTitle>
<p>
<TextBlock>
<Trans i18nKey="pages.stulbe.subtitle">
{' '}
<a href="https://github.com/strimertul/stulbe/">stulbe</a>
</Trans>
</p>
</TextBlock>
</PageHeader>
<Tabs.Root defaultValue="configuration">
<TabContainer defaultValue="configuration">
<TabList>
<TabButton value="configuration">
{t('pages.stulbe.configuration')}
@ -322,7 +323,7 @@ export default function BackendIntegrationPage(): React.ReactElement {
<TabContent value="webhook">
<WebhookIntegration />
</TabContent>
</Tabs.Root>
</TabContainer>
</PageContainer>
);
}

View file

@ -0,0 +1,374 @@
import { CheckIcon } from '@radix-ui/react-icons';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useModule, useStatus } from '../../lib/react-utils';
import apiReducer, { modules } from '../../store/api/reducer';
import DefinitionTable from '../components/DefinitionTable';
import SaveButton from '../components/utils/SaveButton';
import {
Checkbox,
CheckboxIndicator,
Field,
FieldNote,
FlexRow,
InputBox,
Label,
PageContainer,
PageHeader,
PageTitle,
SectionHeader,
styled,
TabButton,
TabContainer,
TabContent,
TabList,
TextBlock,
} from '../theme';
const StepList = styled('ul', {
lineHeight: '1.5',
listStyleType: 'none',
listStylePosition: 'outside',
});
const Step = styled('li', {
marginBottom: '0.5rem',
paddingLeft: '1rem',
'&::marker': {
color: '$teal11',
content: '▧',
display: 'inline-block',
marginLeft: '-0.5rem',
},
});
function TwitchBotSettings() {
const [botConfig, setBotConfig, loadStatus] = useModule(
modules.twitchBotConfig,
);
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
const status = useStatus(loadStatus.save);
const dispatch = useDispatch();
const { t } = useTranslation();
const active = twitchConfig?.enable_bot ?? false;
return (
<form
onSubmit={(ev) => {
dispatch(setTwitchConfig(twitchConfig));
dispatch(setBotConfig(botConfig));
ev.preventDefault();
}}
>
<TextBlock>{t('pages.twitch-settings.bot-settings-copy')}</TextBlock>
<Field>
<FlexRow spacing={1}>
<Checkbox
checked={active}
onCheckedChange={(ev) =>
dispatch(
apiReducer.actions.twitchConfigChanged({
...twitchConfig,
enable_bot: !!ev,
}),
)
}
id="enable-bot"
>
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
</Checkbox>
<Label htmlFor="enable-bot">
{t('pages.twitch-settings.enable-bot')}
</Label>
</FlexRow>
</Field>
<Field size="fullWidth">
<Label htmlFor="bot-channel">
{t('pages.twitch-settings.bot-channel')}
</Label>
<InputBox
type="text"
id="bot-channel"
required={active}
disabled={!active || status?.type === 'pending'}
value={botConfig?.channel ?? ''}
onChange={(ev) =>
dispatch(
apiReducer.actions.twitchBotConfigChanged({
...botConfig,
channel: ev.target.value,
}),
)
}
/>
</Field>
<SectionHeader>
{t('pages.twitch-settings.bot-info-header')}
</SectionHeader>
<Field size="fullWidth">
<Label htmlFor="bot-username">
{t('pages.twitch-settings.bot-username')}
</Label>
<InputBox
type="text"
id="bot-username"
required={active}
disabled={!active || status?.type === 'pending'}
value={botConfig?.username ?? ''}
onChange={(ev) =>
dispatch(
apiReducer.actions.twitchBotConfigChanged({
...botConfig,
username: ev.target.value,
}),
)
}
/>
</Field>
<Field size="fullWidth">
<Label htmlFor="bot-oauth">
{t('pages.twitch-settings.bot-oauth')}
</Label>
<InputBox
type="password"
id="bot-oauth"
required={active}
disabled={!active || status?.type === 'pending'}
value={botConfig?.oauth ?? ''}
onChange={(ev) =>
dispatch(
apiReducer.actions.twitchBotConfigChanged({
...botConfig,
oauth: ev.target.value,
}),
)
}
/>
<FieldNote>
<Trans i18nKey="pages.twitch-settings.bot-oauth-note">
{' '}
<a href="https://twitchapps.com/tmi/">
https://twitchapps.com/tmi/
</a>
</Trans>
</FieldNote>
</Field>
<SectionHeader>
{t('pages.twitch-settings.bot-chat-header')}
</SectionHeader>
<TextBlock>{t('pages.twitch-settings.bot-chat-history-desc')}</TextBlock>
<Field>
<FlexRow spacing={1}>
<Checkbox
disabled={!active || status?.type === 'pending'}
checked={botConfig?.chat_keys ?? false}
onCheckedChange={(ev) =>
dispatch(
apiReducer.actions.twitchBotConfigChanged({
...botConfig,
chat_keys: !!ev,
}),
)
}
id="bot-chat-keys"
>
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
</Checkbox>
<Label htmlFor="bot-chat-keys">
{t('pages.twitch-settings.bot-chat-keys')}
</Label>
</FlexRow>
</Field>
<Field size="vertical">
<Label htmlFor="bot-chat-history">
{t('pages.twitch-settings.bot-chat-history')}
</Label>
<FlexRow
css={{
justifyContent: 'flex-start',
gap: '0.8rem',
backgroundColor: '$gray6',
borderRadius: '5px',
paddingRight: '0.8rem',
}}
>
<InputBox
type="number"
id="bot-chat-history"
required={active}
disabled={!active || status?.type === 'pending'}
value={botConfig?.chat_history ?? ''}
css={{
appearance: 'textfield',
width: '4rem',
textAlign: 'center',
}}
onChange={(ev) =>
dispatch(
apiReducer.actions.twitchBotConfigChanged({
...botConfig,
chat_history: ev.target.value,
}),
)
}
/>
{t('pages.twitch-settings.bot-chat-history-suffix')}
</FlexRow>
</Field>
<SaveButton status={status} />
</form>
);
}
function TwitchAPISettings() {
const { t } = useTranslation();
const [httpConfig] = useModule(modules.httpConfig);
const [twitchConfig, setTwitchConfig, loadStatus] = useModule(
modules.twitchConfig,
);
const status = useStatus(loadStatus.save);
const dispatch = useDispatch();
return (
<form
onSubmit={(ev) => {
dispatch(setTwitchConfig(twitchConfig));
ev.preventDefault();
}}
>
<SectionHeader spacing={'none'}>
{t('pages.twitch-settings.api-subheader')}
</SectionHeader>
<TextBlock>{t('pages.twitch-settings.apiguide-1')}</TextBlock>
<StepList>
<Step>
<Trans i18nKey="pages.twitch-settings.apiguide-2">
{' '}
<a href="https://dev.twitch.tv/console/apps/create">
https://dev.twitch.tv/console/apps/create
</a>
</Trans>
</Step>
<Step>
{t('pages.twitch-settings.apiguide-3')}
<DefinitionTable
entries={{
'OAuth Redirect URLs': `http://${
httpConfig?.bind.indexOf(':') > 0
? httpConfig.bind
: `localhost${httpConfig?.bind ?? ':4337'}`
}/oauth`,
Category: 'Broadcasting Suite',
}}
/>
</Step>
<Step>
<Trans i18nKey="pages.twitch-settings.apiguide-4">
{'str1 '}
<b>str2</b>
</Trans>
</Step>
</StepList>
<Field size="fullWidth" css={{ marginTop: '2rem' }}>
<Label htmlFor="clientid">
{t('pages.twitch-settings.app-client-id')}
</Label>
<InputBox
type="text"
id="clientid"
placeholder={t('pages.twitch-settings.app-client-id')}
required={true}
value={twitchConfig?.api_client_id ?? ''}
onChange={(ev) =>
dispatch(
apiReducer.actions.twitchConfigChanged({
...twitchConfig,
api_client_id: ev.target.value,
}),
)
}
/>
</Field>
<Field size="fullWidth">
<Label htmlFor="clientsecret">
{t('pages.twitch-settings.app-client-secret')}
</Label>
<InputBox
type="password"
id="clientsecret"
placeholder={t('pages.twitch-settings.app-client-secret')}
required={true}
value={twitchConfig?.api_client_secret ?? ''}
onChange={(ev) =>
dispatch(
apiReducer.actions.twitchConfigChanged({
...twitchConfig,
api_client_secret: ev.target.value,
}),
)
}
/>
</Field>
<SaveButton status={status} />
</form>
);
}
export default function TwitchSettingsPage(): React.ReactElement {
const { t } = useTranslation();
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
const dispatch = useDispatch();
const active = twitchConfig?.enabled ?? false;
return (
<PageContainer>
<PageHeader>
<PageTitle>{t('pages.twitch-settings.title')}</PageTitle>
<TextBlock>{t('pages.twitch-settings.subtitle')}</TextBlock>
<Field>
<FlexRow spacing={1}>
<Checkbox
checked={active}
onCheckedChange={(ev) =>
dispatch(
setTwitchConfig({
...twitchConfig,
enabled: !!ev,
}),
)
}
id="enable"
>
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
</Checkbox>
<Label htmlFor="enable">{t('pages.twitch-settings.enable')}</Label>
</FlexRow>
</Field>
</PageHeader>
<div style={{ display: active ? '' : 'none' }}>
<TabContainer defaultValue="api-config">
<TabList>
<TabButton value="api-config">
{t('pages.twitch-settings.api-configuration')}
</TabButton>
<TabButton value="bot-settings">
{t('pages.twitch-settings.bot-settings')}
</TabButton>
</TabList>
<TabContent value="api-config">
<TwitchAPISettings />
</TabContent>
<TabContent value="bot-settings">
<TwitchBotSettings />
</TabContent>
</TabContainer>
</div>
</PageContainer>
);
}

View file

@ -1,4 +1,5 @@
import * as UnstyledLabel from '@radix-ui/react-label';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { styled } from './theme';
export const Field = styled('fieldset', {
@ -6,11 +7,17 @@ export const Field = styled('fieldset', {
marginBottom: '2rem',
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
gap: '0.5rem',
variants: {
size: {
fullWidth: {
flexDirection: 'column',
gap: '0.5rem',
alignItems: 'stretch',
},
vertical: {
flexDirection: 'column',
alignItems: 'flex-start',
},
},
},
@ -95,3 +102,33 @@ export const Button = styled('button', {
},
},
});
export const Checkbox = styled(CheckboxPrimitive.Root, {
all: 'unset',
width: 25,
height: 25,
borderRadius: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid $teal6',
backgroundColor: '$teal4',
'&:hover': {
backgroundColor: '$teal5',
},
'&:active': {
background: '$teal6',
},
'&:disabled': {
backgroundColor: '$gray4',
borderColor: '$gray5',
color: '$gray8',
},
});
export const CheckboxIndicator = styled(CheckboxPrimitive.Indicator, {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '$teal11',
});

View file

@ -3,3 +3,4 @@ export * from './brand';
export * from './forms';
export * from './pages';
export * from './tabs';
export * from './utils';

View file

@ -28,3 +28,7 @@ export const SectionHeader = styled('h2', {
},
},
});
export const TextBlock = styled('p', {
lineHeight: '1.5',
});

View file

@ -1,6 +1,8 @@
import * as Tabs from '@radix-ui/react-tabs';
import { styled } from './theme';
export const TabContainer = styled(Tabs.Root, {});
export const TabList = styled(Tabs.List, {
borderBottom: '1px solid $gray6',
});

View file

@ -8,6 +8,7 @@ import {
import { globalCss, createStitches } from '@stitches/react';
export const globalStyles = globalCss({
'*': { boxSizing: 'border-box' },
body: { margin: 0, padding: 0, backgroundColor: '$gray1', color: '$teal12' },
html: {
margin: 0,

View file

@ -0,0 +1,15 @@
import { styled } from './theme';
export const FlexRow = styled('div', {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
variants: {
spacing: {
'1': {
gap: '0.5rem',
},
},
},
});