feat(@ui): migrate textarea and tip

This commit is contained in:
Paul Makles 2022-05-30 12:47:13 +01:00
parent b4777e9816
commit f3bdbe52d9
14 changed files with 41 additions and 607 deletions

View file

@ -1,31 +0,0 @@
.container {
font-size: .875rem;
line-height: 20px;
position: relative;
}
.textarea {
width: 100%;
white-space: pre-wrap;
textarea::placeholder {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.hide {
width: 100%;
overflow: hidden;
position: relative;
}
.ghost {
width: 100%;
white-space: pre-wrap;
top: 0;
position: absolute;
visibility: hidden;
}

View file

@ -1,58 +0,0 @@
import styled, { css } from "styled-components/macro";
export interface TextAreaProps {
code?: boolean;
padding?: string;
lineHeight?: string;
hideBorder?: boolean;
}
export const TEXT_AREA_BORDER_WIDTH = 2;
export const DEFAULT_TEXT_AREA_PADDING = 16;
export const DEFAULT_LINE_HEIGHT = 20;
export default styled.textarea<TextAreaProps>`
width: 100%;
resize: none;
display: block;
color: var(--foreground);
background: var(--secondary-background);
padding: ${(props) => props.padding ?? "var(--textarea-padding)"};
line-height: ${(props) =>
props.lineHeight ?? "var(--textarea-line-height)"};
${(props) =>
props.hideBorder &&
css`
border: none;
`}
${(props) =>
!props.hideBorder &&
css`
border-radius: var(--border-radius);
transition: border-color 0.2s ease-in-out;
border: var(--input-border-width) solid transparent;
`}
&:focus {
outline: none;
${(props) =>
!props.hideBorder &&
css`
border: var(--input-border-width) solid var(--accent);
`}
}
${(props) =>
props.code
? css`
font-family: var(--monospace-font), monospace;
`
: css`
font-family: inherit;
`}
font-variant-ligatures: var(--ligatures);
`;

View file

@ -1,72 +0,0 @@
import { InfoCircle } from "@styled-icons/boxicons-regular";
import styled, { css } from "styled-components/macro";
import { Children } from "../../types/Preact";
interface Props {
warning?: boolean;
error?: boolean;
}
export const Separator = styled.div<Props>`
height: 1px;
width: calc(100% - 10px);
background: var(--secondary-header);
margin: 18px auto;
`;
export const TipBase = styled.div<Props>`
display: flex;
padding: 12px;
font-weight: 500;
overflow: hidden;
align-items: center;
font-size: 14px;
background: var(--primary-header);
border-radius: var(--border-radius);
border: 2px solid var(--secondary-header);
a {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
svg {
flex-shrink: 0;
margin-inline-end: 10px;
}
${(props) =>
props.warning &&
css`
color: var(--warning);
border: 2px solid var(--warning);
background: var(--secondary-header);
`}
${(props) =>
props.error &&
css`
color: white;
border: 2px solid var(--error);
background: var(--error);
`}
`;
export default function Tip(
props: Props & { children: Children; hideSeparator?: boolean },
) {
const { children, hideSeparator, ...tipProps } = props;
return (
<>
{!hideSeparator && <Separator />}
<TipBase {...tipProps}>
<InfoCircle size={20} />
<span>{children}</span>
</TipBase>
</>
);
}

View file

@ -3,7 +3,8 @@ import styled from "styled-components/macro";
import { RefObject } from "preact"; import { RefObject } from "preact";
import { useEffect, useLayoutEffect, useRef } from "preact/hooks"; import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
import TextArea, { TextAreaProps } from "../components/ui/TextArea"; import { TextArea } from "@revoltchat/ui";
import type { TextAreaProps } from "@revoltchat/ui/esm/components/design/atoms/inputs/TextArea";
import { internalSubscribe } from "./eventEmitter"; import { internalSubscribe } from "./eventEmitter";
import { isTouchscreenDevice } from "./isTouchscreenDevice"; import { isTouchscreenDevice } from "./isTouchscreenDevice";

View file

@ -4,14 +4,13 @@ import styled from "styled-components/macro";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { Button, ComboBox, Preloader } from "@revoltchat/ui"; import { Button, ComboBox, Preloader, Tip } from "@revoltchat/ui";
import { useClient } from "../../context/revoltjs/RevoltClient"; import { useClient } from "../../context/revoltjs/RevoltClient";
import UserIcon from "../../components/common/user/UserIcon"; import UserIcon from "../../components/common/user/UserIcon";
import Markdown from "../../components/markdown/Markdown"; import Markdown from "../../components/markdown/Markdown";
import Overline from "../../components/ui/Overline"; import Overline from "../../components/ui/Overline";
import Tip from "../../components/ui/Tip";
const BotInfo = styled.div` const BotInfo = styled.div`
gap: 12px; gap: 12px;
@ -46,9 +45,7 @@ export default function InviteBot() {
return ( return (
<div style={{ padding: "6em" }}> <div style={{ padding: "6em" }}>
<Tip warning hideSeparator> <Tip palette="warning">This section is under construction.</Tip>
This section is under construction.
</Tip>
{typeof data === "undefined" && <Preloader type="spinner" />} {typeof data === "undefined" && <Preloader type="spinner" />}
{data && ( {data && (
<> <>

View file

@ -14,7 +14,7 @@ import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import { Button } from "@revoltchat/ui"; import { Button, LineDivider, Tip } from "@revoltchat/ui";
import { stopPropagation } from "../../../lib/stopPropagation"; import { stopPropagation } from "../../../lib/stopPropagation";
@ -27,7 +27,6 @@ import {
import Tooltip from "../../../components/common/Tooltip"; import Tooltip from "../../../components/common/Tooltip";
import UserIcon from "../../../components/common/user/UserIcon"; import UserIcon from "../../../components/common/user/UserIcon";
import Tip from "../../../components/ui/Tip";
import CategoryButton from "../../../components/ui/fluent/CategoryButton"; import CategoryButton from "../../../components/ui/fluent/CategoryButton";
export const Account = observer(() => { export const Account = observer(() => {
@ -227,6 +226,7 @@ export const Account = observer(() => {
<Text id="app.settings.pages.account.manage.delete" /> <Text id="app.settings.pages.account.manage.delete" />
</CategoryButton> </CategoryButton>
</a> </a>
<LineDivider />
<Tip> <Tip>
<span> <span>
<Text id="app.settings.tips.account.a" /> <Text id="app.settings.tips.account.a" />

View file

@ -2,7 +2,7 @@ import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { Button, ComboBox } from "@revoltchat/ui"; import { Button, ComboBox, Tip } from "@revoltchat/ui";
import { stopPropagation } from "../../../lib/stopPropagation"; import { stopPropagation } from "../../../lib/stopPropagation";
import { voiceState } from "../../../lib/vortex/VoiceState"; import { voiceState } from "../../../lib/vortex/VoiceState";
@ -10,7 +10,6 @@ import { voiceState } from "../../../lib/vortex/VoiceState";
import opusSVG from "../assets/opus_logo.svg"; import opusSVG from "../assets/opus_logo.svg";
import Overline from "../../../components/ui/Overline"; import Overline from "../../../components/ui/Overline";
import Tip from "../../../components/ui/Tip";
{ {
/*import OpusSVG from "../assets/opus_logo.svg";*/ /*import OpusSVG from "../assets/opus_logo.svg";*/
@ -96,13 +95,13 @@ export function Audio() {
<> <>
<div class={styles.audio}> <div class={styles.audio}>
{!permission && ( {!permission && (
<Tip error hideSeparator> <Tip palette="error">
<Text id="app.settings.pages.audio.tip_grant_permission" /> <Text id="app.settings.pages.audio.tip_grant_permission" />
</Tip> </Tip>
)} )}
{error && permission === "prompt" && ( {error && permission === "prompt" && (
<Tip error hideSeparator> <Tip palette="error">
<Text id="app.settings.pages.audio.tip_retry" /> <Text id="app.settings.pages.audio.tip_retry" />
<a onClick={handleAskForPermission}> <a onClick={handleAskForPermission}>
<Text id="app.settings.pages.audio.button_retry" /> <Text id="app.settings.pages.audio.button_retry" />

View file

@ -5,7 +5,7 @@ import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useMemo } from "preact/hooks"; import { useMemo } from "preact/hooks";
import { Checkbox } from "@revoltchat/ui"; import { Checkbox, LineDivider, Tip } from "@revoltchat/ui";
import { useApplicationState } from "../../../mobx/State"; import { useApplicationState } from "../../../mobx/State";
@ -23,7 +23,6 @@ import {
Languages as Langs, Languages as Langs,
} from "../../../../external/lang/Languages"; } from "../../../../external/lang/Languages";
import Emoji from "../../../components/common/Emoji"; import Emoji from "../../../components/common/Emoji";
import Tip from "../../../components/ui/Tip";
type Key = [Language, LanguageEntry]; type Key = [Language, LanguageEntry];
@ -198,16 +197,17 @@ export const Languages = observer(() => {
.filter(([, lang]) => lang.cat === "alt") .filter(([, lang]) => lang.cat === "alt")
.map(EntryFactory)} .map(EntryFactory)}
</div> </div>
<LineDivider />
<Tip> <Tip>
<span> <span>
<Text id="app.settings.tips.languages.a" /> <Text id="app.settings.tips.languages.a" />{" "}
</span>{" "} <a
<a href="https://weblate.insrt.uk/engage/revolt/?utm_source=widget"
href="https://weblate.insrt.uk/engage/revolt/?utm_source=widget" target="_blank"
target="_blank" rel="noreferrer">
rel="noreferrer"> <Text id="app.settings.tips.languages.b" />
<Text id="app.settings.tips.languages.b" /> </a>
</a> </span>
</Tip> </Tip>
</div> </div>
); );

View file

@ -11,7 +11,7 @@ import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useCallback, useEffect, useState } from "preact/hooks"; import { useCallback, useEffect, useState } from "preact/hooks";
import { Button, Checkbox, InputBox } from "@revoltchat/ui"; import { Button, Checkbox, InputBox, Tip } from "@revoltchat/ui";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { internalEmit } from "../../../lib/eventEmitter"; import { internalEmit } from "../../../lib/eventEmitter";
@ -28,7 +28,6 @@ import AutoComplete, {
import CollapsibleSection from "../../../components/common/CollapsibleSection"; import CollapsibleSection from "../../../components/common/CollapsibleSection";
import Tooltip from "../../../components/common/Tooltip"; import Tooltip from "../../../components/common/Tooltip";
import UserIcon from "../../../components/common/user/UserIcon"; import UserIcon from "../../../components/common/user/UserIcon";
import Tip from "../../../components/ui/Tip";
import CategoryButton from "../../../components/ui/fluent/CategoryButton"; import CategoryButton from "../../../components/ui/fluent/CategoryButton";
interface Data { interface Data {
@ -465,9 +464,7 @@ function BotCard({ bot, onDelete, onUpdate }: Props) {
{error && ( {error && (
<div className={styles.botSection}> <div className={styles.botSection}>
<Tip error hideSeparator> <Tip palette="error">{error}</Tip>
{error}
</Tip>
</div> </div>
)} )}

View file

@ -2,11 +2,10 @@ import { Refresh } from "@styled-icons/boxicons-regular";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { Button, Checkbox } from "@revoltchat/ui"; import { Button, Checkbox, Tip } from "@revoltchat/ui";
import RLogo from "../assets/revolt_r.svg"; import RLogo from "../assets/revolt_r.svg";
import Tip from "../../../components/ui/Tip";
import CategoryButton from "../../../components/ui/fluent/CategoryButton"; import CategoryButton from "../../../components/ui/fluent/CategoryButton";
export function Native() { export function Native() {
@ -27,7 +26,7 @@ export function Native() {
return ( return (
<div style={{ marginTop: "10px" }}> <div style={{ marginTop: "10px" }}>
<Tip hideSeparator>Some options might require a restart.</Tip> <Tip>Some options might require a restart.</Tip>
<h3>App Behavior</h3> <h3>App Behavior</h3>
<Checkbox <Checkbox
value={autoStart ?? false} value={autoStart ?? false}

View file

@ -3,12 +3,10 @@ import { observer } from "mobx-react-lite";
import styles from "./Panes.module.scss"; import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { Button, Checkbox } from "@revoltchat/ui"; import { Button, Checkbox, Tip } from "@revoltchat/ui";
import { useApplicationState } from "../../../mobx/State"; import { useApplicationState } from "../../../mobx/State";
import Tip from "../../../components/ui/Tip";
// Just keeping this here for general purpose. Should probably be exported // Just keeping this here for general purpose. Should probably be exported
// elsewhere, though. // elsewhere, though.
interface Plugin { interface Plugin {
@ -69,7 +67,7 @@ export const PluginsPage = observer(() => {
const plugins = useApplicationState().plugins; const plugins = useApplicationState().plugins;
return ( return (
<div className={styles.plugins}> <div className={styles.plugins}>
<Tip error hideSeparator> <Tip palette="error">
<Text id="app.settings.pages.plugins.wip" /> <Text id="app.settings.pages.plugins.wip" />
</Tip> </Tip>
{plugins.list().map((plugin) => { {plugins.list().map((plugin) => {

View file

@ -7,7 +7,7 @@ import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useCallback, useContext, useEffect, useState } from "preact/hooks"; import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { Button } from "@revoltchat/ui"; import { Button, LineDivider, Tip } from "@revoltchat/ui";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { useTranslation } from "../../../lib/i18n"; import { useTranslation } from "../../../lib/i18n";
@ -23,7 +23,6 @@ import {
import AutoComplete, { import AutoComplete, {
useAutoComplete, useAutoComplete,
} from "../../../components/common/AutoComplete"; } from "../../../components/common/AutoComplete";
import Tip from "../../../components/ui/Tip";
export const Profile = observer(() => { export const Profile = observer(() => {
const status = useContext(StatusContext); const status = useContext(StatusContext);
@ -203,11 +202,15 @@ export const Profile = observer(() => {
<Text id="app.special.modals.actions.save" /> <Text id="app.special.modals.actions.save" />
</Button> </Button>
</p> </p>
<LineDivider />
<Tip> <Tip>
<span>Want to change your username?</span>{" "} <span>
<a onClick={() => switchPage("account")}> Want to change your username?{" "}
Head over to your account settings. <a onClick={() => switchPage("account")}>
</a> Head over to your account settings.
</a>
</span>
</Tip> </Tip>
</div> </div>
); );

View file

@ -18,13 +18,12 @@ import styles from "./Panes.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import { Button, Preloader } from "@revoltchat/ui"; import { Button, LineDivider, Preloader, Tip } from "@revoltchat/ui";
import { dayjs } from "../../../context/Locale"; import { dayjs } from "../../../context/Locale";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Tip from "../../../components/ui/Tip";
import CategoryButton from "../../../components/ui/fluent/CategoryButton"; import CategoryButton from "../../../components/ui/fluent/CategoryButton";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
@ -248,13 +247,14 @@ export function Sessions() {
<Text id="app.settings.pages.sessions.logout" /> <Text id="app.settings.pages.sessions.logout" />
</CategoryButton> </CategoryButton>
<LineDivider />
<Tip> <Tip>
<span> <span>
<Text id="app.settings.tips.sessions.a" /> <Text id="app.settings.tips.sessions.a" />{" "}
</span>{" "} <a onClick={() => switchPage("account")}>
<a onClick={() => switchPage("account")}> <Text id="app.settings.tips.sessions.b" />
<Text id="app.settings.tips.sessions.b" /> </a>
</a> </span>
</Tip> </Tip>
</div> </div>
); );

View file

@ -1,399 +0,0 @@
/**
* ! DEPRECATED FILE
* ! DO NOT IMPORT
*
* Replaced by Revolt Discover
*/
import { Check } from "@styled-icons/boxicons-regular";
import {
Star,
Brush,
Bookmark,
BarChartAlt2,
} from "@styled-icons/boxicons-solid";
import styled from "styled-components/macro";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import { useApplicationState } from "../../../mobx/State";
import { Theme, generateVariables } from "../../../context/Theme";
import Tip from "../../../components/ui/Tip";
import previewPath from "../assets/preview.svg";
import { GIT_REVISION } from "../../../revision";
export const fetchManifest = (): Promise<Manifest> =>
fetch(`${import.meta.env.VITE_THEMES_URL}/manifest.json`).then((res) =>
res.json(),
);
export const fetchTheme = (slug: string): Promise<Theme> =>
fetch(`${import.meta.env.VITE_THEMES_URL}/theme_${slug}.json`).then((res) =>
res.json(),
);
export interface ThemeMetadata {
name: string;
creator: string;
commit?: string;
description: string;
}
export type Manifest = {
generated: string;
themes: Record<string, ThemeMetadata>;
};
// TODO: ability to preview / display the settings set like in the appearance pane
const ThemeInfo = styled.article`
display: flex;
flex-direction: column;
gap: 10px;
padding: 1rem;
border-radius: var(--border-radius);
background: var(--secondary-background);
&[data-loaded] {
.preview {
opacity: 1;
}
}
.preview {
grid-area: preview;
aspect-ratio: 323 / 202;
background-color: var(--secondary-background);
border-radius: calc(var(--border-radius) / 2);
// prep style for later
outline: 3px solid transparent;
// hide random svg parts, crop border on firefox
overflow: hidden;
// hide until loaded
opacity: 0;
// style button
border: 0;
margin: 0;
padding: 0;
transition: 0.25s opacity, 0.25s outline;
> * {
grid-area: 1 / 1;
}
svg {
height: 100%;
width: 100%;
object-fit: contain;
}
&:hover,
&:active,
&:focus-visible {
outline: 3px solid var(--tertiary-background);
}
}
.name {
margin-top: 5px !important;
grid-area: name;
margin: 0;
}
.creator {
grid-area: creator;
justify-self: end;
font-size: 0.75rem;
}
.description {
margin-bottom: 5px;
grid-area: desc;
}
.previewBox {
position: relative;
height: 100%;
width: 100%;
.hover {
opacity: 0;
font-family: var(--font), sans-serif;
font-variant-ligatures: var(--ligatures);
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
color: white;
height: 100%;
width: 100%;
z-index: 10;
position: absolute;
background: rgba(0, 0, 0, 0.5);
cursor: pointer;
transition: opacity 0.2s ease-in-out;
&:hover {
opacity: 1;
}
}
> svg {
height: 100%;
}
}
`;
const ThemeList = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
`;
const Banner = styled.div`
display: flex;
flex-direction: column;
`;
const Category = styled.div`
display: flex;
gap: 8px;
align-items: center;
.title {
display: flex;
align-items: center;
gap: 8px;
flex-grow: 1;
}
.view {
font-size: 12px;
}
`;
const ActiveTheme = styled.div`
display: flex;
flex-direction: column;
background: var(--secondary-background);
padding: 0;
border-radius: var(--border-radius);
gap: 8px;
overflow: hidden;
.active-indicator {
display: flex;
gap: 6px;
align-items: center;
background: var(--accent);
width: 100%;
padding: 5px 10px;
font-size: 13px;
font-weight: 400;
color: white;
}
.title {
font-size: 1.2rem;
font-weight: 600;
}
.author {
font-size: 12px;
margin-bottom: 5px;
}
.theme {
width: 124px;
height: 80px;
background: var(--tertiary-background);
border-radius: 4px;
}
.container {
display: flex;
gap: 16px;
padding: 10px 16px 16px;
}
`;
const ThemedSVG = styled.svg<{ theme: Theme }>`
${(props) => props.theme && generateVariables(props.theme)}
`;
type ThemePreviewProps = Omit<JSX.HTMLAttributes<SVGSVGElement>, "as"> & {
slug?: string;
theme?: Theme;
onThemeLoaded?: (theme: Theme) => void;
};
const ThemePreview = ({ theme, ...props }: ThemePreviewProps) => {
return (
<ThemedSVG
{...props}
theme={theme}
width="323"
height="202"
aria-hidden="true"
data-loaded={!!theme}>
<use href={`${previewPath}#preview`} width="100%" height="100%" />
</ThemedSVG>
);
};
const ThemeShopRoot = styled.div`
display: grid;
gap: 1rem;
h5 {
margin-bottom: 0;
}
`;
export function ThemeShop() {
// setThemeList is for adding more / lazy loading in the future
const [themeList, setThemeList] = useState<
[string, ThemeMetadata][] | null
>(null);
const [themeData, setThemeData] = useState<Record<string, Theme>>({});
const themes = useApplicationState().settings.theme;
async function fetchThemeList() {
const manifest = await fetchManifest();
setThemeList(
Object.entries(manifest.themes).filter((x) =>
x[1].commit ? x[1].commit === GIT_REVISION : true,
),
);
}
async function getTheme(slug: string) {
const theme = await fetchTheme(slug);
setThemeData((data) => ({ ...data, [slug]: theme }));
}
useEffect(() => {
fetchThemeList();
}, []);
useEffect(() => {
themeList?.forEach(([slug]) => {
getTheme(slug);
});
}, [themeList]);
return (
<ThemeShopRoot>
<h5>
<Text id="app.settings.pages.theme_shop.description" />
</h5>
{/*<LoadFail>
<h5>
Oops! Couldn't load the theme shop. Make sure you're
connected to the internet and try again.
</h5>
</LoadFail>*/}
<Tip warning hideSeparator>
The Theme Shop is currently under construction.
</Tip>
{/* FIXME INTEGRATE WITH MOBX */}
{/*<ActiveTheme>
<div class="active-indicator">
<Check size="16" />
<Text id="app.settings.pages.theme_shop.active" />
</div>
<div class="container">
<div class="theme">theme svg goes here</div>
<div class="info">
<div class="title">Theme Title</div>
<div class="author">
<Text id="app.settings.pages.theme_shop.by" />{" "}
Author
</div>
<h5>This is a theme description.</h5>
</div>
</div>
</ActiveTheme>
<InputBox placeholder="<Text id="app.settings.pages.theme_shop.search" />" contrast />
<Category>
<div class="title">
<Bookmark size={16} />
<Text id="app.settings.pages.theme_shop.category.saved" />
</div>
<a class="view">
<Text id="app.settings.pages.theme_shop.category.manage" />
</a>
</Category>
<Category>
<div class="title">
<Star size={16} />
<Text id="app.settings.pages.theme_shop.category.new" />
</div>
<a class="view">
<Text id="app.settings.pages.theme_shop.category.viewall" />
</a>
</Category>
<Category>
<div class="title">
<BarChartAlt2 size={16} />
<Text id="app.settings.pages.theme_shop.category.highest" />
</div>
<a class="view">
<Text id="app.settings.pages.theme_shop.category.viewall" />
</a>
</Category>
<Category>
<div class="title">
<Brush size={16} />
<Text id="app.settings.pages.theme_shop.category.default" />
</div>
<a class="view">
<Text id="app.settings.pages.theme_shop.category.viewall" />
</a>
</Category>*/}
<hr />
<ThemeList>
{themeList?.map(([slug, theme]) => (
<ThemeInfo
key={slug}
data-loaded={Reflect.has(themeData, slug)}>
<button
class="preview"
onClick={() =>
themes.hydrate(themeData[slug], true)
}>
<div class="previewBox">
<div class="hover">
<Text id="app.settings.pages.theme_shop.use" />
</div>
<ThemePreview
slug={slug}
theme={themeData[slug]}
/>
</div>
</button>
<h1 class="name">{theme.name}</h1>
{/* Maybe id's of the users should be included as well / instead? */}
<div class="creator">
<Text id="app.settings.pages.theme_shop.by" />{" "}
{theme.creator}
</div>
<h5 class="description">{theme.description}</h5>
</ThemeInfo>
))}
</ThemeList>
</ThemeShopRoot>
);
}