diff --git a/makefile b/makefile index d58e101..2585cc8 100644 --- a/makefile +++ b/makefile @@ -29,6 +29,7 @@ move: clean: mkdir -p ./dist rm -r ./dist + rm -r ./db reset-server: psql postgres postgres < ./src/server/sql/init.sql diff --git a/src/client/webapp/elements/components/button.tsx b/src/client/webapp/elements/components/button.tsx index 53a9eab..f83b38e 100644 --- a/src/client/webapp/elements/components/button.tsx +++ b/src/client/webapp/elements/components/button.tsx @@ -1,14 +1,41 @@ -import React, { FC } from 'react'; +import React, { FC, useCallback, useMemo } from 'react'; + +export enum ButtonType { + BRAND = '', + POSITIVE = 'positive', + NEGATIVE = 'negative', + PERDU = 'perdu', +} interface ButtonProps { - onClick: () => void + type?: ButtonType; + onClick?: () => void; shaking?: boolean; - children: React.ReactNode + children?: React.ReactNode; +} + +const DefaultButtonProps: ButtonProps = { + type: ButtonType.BRAND } const Button: FC = (props: ButtonProps) => { - const { onClick, shaking, children } = props; - return
{children}
+ const { type, onClick, shaking, children } = { ...DefaultButtonProps, ...props }; + + const className = useMemo( + () => [ + 'button', + type, + shaking && 'shaking-horizontal', + ].filter(c => typeof c === 'string').join(' '), + [ type, shaking ] + ); + + const clickHandler = useCallback(() => { + if (shaking) return; // ignore clicks while shaking + if (onClick) onClick(); + }, [ shaking, onClick ]); + + return
{children}
} export default Button; \ No newline at end of file diff --git a/src/client/webapp/elements/components/display-popup.tsx b/src/client/webapp/elements/components/display-popup.tsx new file mode 100644 index 0000000..0cfc9e4 --- /dev/null +++ b/src/client/webapp/elements/components/display-popup.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; + +export interface DisplayPopupProps { + tip: string | null; + children?: React.ReactNode; // buttons +} + +const DisplayPopup: FC = (props: DisplayPopupProps) => { + const { tip, children } = props; + + return ( +
+
+
{tip}
+
+ {children} +
+
+
+ ); +}; + +export default DisplayPopup; diff --git a/src/client/webapp/elements/components/display.tsx b/src/client/webapp/elements/components/display.tsx new file mode 100644 index 0000000..76f9431 --- /dev/null +++ b/src/client/webapp/elements/components/display.tsx @@ -0,0 +1,75 @@ +import * as electronRemote from '@electron/remote'; +const electronConsole = electronRemote.getGlobal('console') as Console; +import Logger from '../../../../logger/logger'; +const LOG = Logger.create(__filename, electronConsole); + +import React, { FC, useEffect, useMemo, useState } from "react"; +import ElementsUtil from "../require/elements-util"; +import Button, { ButtonType } from "./button"; +import DisplayPopup from "./display-popup"; + +interface DisplayProps { + children: React.ReactNode; + + changes: boolean; + resetChanges: () => void; + saveChanges: () => void; + + saving: boolean; + saveFailed: boolean; + errorMessage: string | null; +} + +const Display: FC = (props: DisplayProps) => { + const { children, changes, resetChanges, saveChanges, saving, saveFailed, errorMessage } = props; + + const [ saveButtonShaking, setSaveButtonShaking ] = useState(false); + + useEffect(() => { + (async () => { + if (saveFailed) { + await ElementsUtil.delayToggleState(setSaveButtonShaking, 400); + } + })(); + }, [ saveFailed ]); + + const changesButtonText = useMemo(() => { + if (saving) { + return 'Saving...'; + } else if (saveFailed) { + return 'Try Again'; + } else { + return 'Save Changes'; + } + }, [ saving, saveFailed ]); + + const popup = useMemo(() => { + if (errorMessage) { + return ( + + + + ); + } else if (changes) { + return ( + + + + + ); + } else { + return null; + } + }, [ errorMessage, changes, resetChanges, saveChanges ]); + + return ( +
+
+ {children} +
+ {popup} +
+ ); +} + +export default Display; diff --git a/src/client/webapp/elements/components/input-image-edit.tsx b/src/client/webapp/elements/components/input-image-edit.tsx new file mode 100644 index 0000000..5a77327 --- /dev/null +++ b/src/client/webapp/elements/components/input-image-edit.tsx @@ -0,0 +1,93 @@ +import * as electronRemote from '@electron/remote'; +const electronConsole = electronRemote.getGlobal('console') as Console; +import Logger from '../../../../logger/logger'; +const LOG = Logger.create(__filename, electronConsole); + +import React, { FC, useEffect, useRef, useState } from 'react'; +import ElementsUtil from '../require/elements-util'; + +import * as FileType from 'file-type'; + +import * as crypto from 'crypto'; + +interface ImageEditInputProps { + alt?: string; + + maxSize: number; + + value: Buffer | null; + setValue: React.Dispatch>; + + setErrorMessage: React.Dispatch>; +} + +const ImageEditInput: FC = (props: ImageEditInputProps) => { + const { alt, maxSize, value, setValue, setErrorMessage } = props; + + const acceptedExtTypes = [ 'png', 'jpg', 'jpeg' ]; + + const imgRef = useRef(null); + + const [ imgSrc, setImgSrc ] = useState('./img/loading.svg'); + + useEffect(() => { + (async () => { + if (value) { + try { + const src = await ElementsUtil.getImageBufferSrc(value); + setImgSrc(src); + } catch (e: unknown) { + LOG.error('unable to get image buffer src', e); + setImgSrc('./img/error.png'); + setErrorMessage('Unable to get image src'); + } + } else { + setImgSrc('./img/loading.svg'); + } + })(); + }, [ value ]); + + useEffect(() => { + if (imgRef.current) { + imgRef.current.src = imgSrc; + } + }, [ imgSrc ]) + + const handleFileChange = async (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) { + LOG.debug('no files'); + return; + } + const file = files[0] as File; + if (file.size > maxSize) { + e.target.value = ''; + LOG.debug('image too large'); + setErrorMessage(`Image too large. (${ElementsUtil.humanSize(file.size)}>${ElementsUtil.humanSize(maxSize)})`); + return; + } + const buff = Buffer.from(await file.arrayBuffer()); + const typeResult = await FileType.fromBuffer(buff); + if (!typeResult || !acceptedExtTypes.includes(typeResult.ext)) { + e.target.value = ''; + setErrorMessage(`Invalid image type. Accepted types: ${acceptedExtTypes.join(', ')}`); + return; + } + setErrorMessage(null); + setValue(buff); + } + + return ( +
+ +
+ ); +} + +export default ImageEditInput; diff --git a/src/client/webapp/elements/components/input-text.tsx b/src/client/webapp/elements/components/input-text.tsx new file mode 100644 index 0000000..90aedad --- /dev/null +++ b/src/client/webapp/elements/components/input-text.tsx @@ -0,0 +1,43 @@ +import * as electronRemote from '@electron/remote'; +const electronConsole = electronRemote.getGlobal('console') as Console; +import Logger from '../../../../logger/logger'; +const LOG = Logger.create(__filename, electronConsole); + +import React, { FC, useMemo, useState } from 'react'; + +export interface TextInputProps { + label?: string; + placeholder?: string; + + value: string; + setValue: React.Dispatch>; + + onEnterKeyDown?: () => void; +} + +const TextInput: FC = (props: TextInputProps) => { + const { label, placeholder, value, setValue, onEnterKeyDown } = props; + + const labelElement = useMemo(() => { + return label &&
{label}
+ }, [ label ]); + + const handleChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + if (onEnterKeyDown) onEnterKeyDown(); + } + } + + return ( +
{/* TODO: remove -react */} + {labelElement} + +
+ ) +} + +export default TextInput; diff --git a/src/client/webapp/elements/components/overlay.tsx b/src/client/webapp/elements/components/overlay.tsx index e0dcbec..fdacaac 100644 --- a/src/client/webapp/elements/components/overlay.tsx +++ b/src/client/webapp/elements/components/overlay.tsx @@ -26,22 +26,12 @@ const Overlay: FC = (props: OverlayProps) => { if (node.current) { ReactDOM.unmountComponentAtNode(node.current.parentElement as Element); } - if (keyDownEventHandler) { - window.removeEventListener('keydown', keyDownEventHandler); + if (keyDownHandler) { + window.removeEventListener('keydown', keyDownHandler); } // otherwise, this isn't in the DOM anyway }; - const keyDownEventHandler = useMemo(() => { - const eventHandler = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - removeSelf(); - } - }; - window.addEventListener('keydown', eventHandler); - return eventHandler; - }, []); - const checkMouseDown = (e: React.MouseEvent) => { if (e.target === node.current) { setMouseDownInChild(false); @@ -58,7 +48,23 @@ const Overlay: FC = (props: OverlayProps) => { } }; - return
{children}
+ const keyDownHandler = useMemo(() => { + const eventHandler = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + removeSelf(); + } + }; + window.addEventListener('keydown', eventHandler); + return eventHandler; + }, []); + + const clickHandler = (e: React.MouseEvent) => { + if (e.target === node.current) { + removeSelf(); + } + } + + return
{children}
}; export default Overlay; diff --git a/src/client/webapp/elements/context-menu-guild-title.tsx b/src/client/webapp/elements/context-menu-guild-title.tsx index 5d7376f..6cc835f 100644 --- a/src/client/webapp/elements/context-menu-guild-title.tsx +++ b/src/client/webapp/elements/context-menu-guild-title.tsx @@ -10,7 +10,6 @@ import Q from '../q-module'; import UI from '../ui'; import createErrorMessageOverlay from './overlay-error-message'; -import createGuildSettingsOverlay from './overlay-guild-settings'; import createCreateInviteTokenOverlay from './overlay-create-invite-token'; import createCreateChannelOverlay from './overlay-create-channel'; import createTokenLogOverlay from './overlay-token-log'; @@ -18,6 +17,7 @@ import CombinedGuild from '../guild-combined'; import React from 'react'; import ReactHelper from './require/react-helper'; +import GuildSettingsOverlay from './overlays/overlay-guild-settings'; export default function createGuildTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): Element { if (ui.activeConnection === null) { @@ -25,7 +25,6 @@ export default function createGuildTitleContextMenu(document: Document, q: Q, ui return ReactHelper.createElementFromJSX(
); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any const menuItems: JSX.Element[] = []; if (ui.activeConnection.privileges.includes('modify_profile')) { @@ -87,8 +86,9 @@ export default function createGuildTitleContextMenu(document: Document, q: Q, ui const overlay = createErrorMessageOverlay(document, 'Error Opening Settings', 'Could not load guild information'); document.body.appendChild(overlay); } else { - const overlay = createGuildSettingsOverlay(document, q, guild, guildMeta); - document.body.appendChild(overlay); + ElementsUtil.presentReactOverlay(document, ); + // const overlay = createGuildSettingsOverlay(document, q, guild, guildMeta); + // document.body.appendChild(overlay); } }); } diff --git a/src/client/webapp/elements/displays/display-guild-overview.tsx b/src/client/webapp/elements/displays/display-guild-overview.tsx new file mode 100644 index 0000000..231d23e --- /dev/null +++ b/src/client/webapp/elements/displays/display-guild-overview.tsx @@ -0,0 +1,129 @@ +import * as electronRemote from '@electron/remote'; +const electronConsole = electronRemote.getGlobal('console') as Console; +import Logger from '../../../../logger/logger'; +const LOG = Logger.create(__filename, electronConsole); + +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; + +import { GuildMetadata } from '../../data-types'; +import Globals from '../../globals'; +import CombinedGuild from '../../guild-combined'; +import Display from '../components/display'; +import ElementsUtil from '../require/elements-util'; +import TextInput from '../components/input-text'; +import ImageEditInput from '../components/input-image-edit'; + +export interface GuildOverviewDisplayProps { + guild: CombinedGuild; + guildMeta: GuildMetadata; +} +const GuildOverviewDisplay: FC = (props: GuildOverviewDisplayProps) => { + const { guild, guildMeta } = props; + + const [ savedName, setSavedName ] = useState(guildMeta.name); + const [ savedIconBuff, setSavedIconBuff ] = useState(null); + + const [ name, setName ] = useState(guildMeta.name); + const [ iconBuff, setIconBuff ] = useState(null); + + const [ saving, setSaving ] = useState(false); + + const [ iconFailed, setIconFailed ] = useState(false); + const [ saveFailed, setSaveFailed ] = useState(false); + + const [ imageInputErrorMessage, setImageInputErrorMessage ] = useState(null); + + useEffect(() => { + (async () => { + try { + const iconResource = await guild.fetchResource(guildMeta.iconResourceId); + setSavedIconBuff(iconResource.data); + setIconBuff(iconResource.data); + } catch (e: unknown) { + LOG.error('Error loading icon resource', e); + setIconFailed(true); + } + })(); + }, []); + + const changes = useMemo(() => { + return name !== savedName || iconBuff?.toString('hex') !== savedIconBuff?.toString('hex') + }, [ name, savedName, iconBuff, savedIconBuff ]); + + const errorMessage = useMemo(() => { + if (iconFailed) { + return 'Unable to load icon'; + } else if (imageInputErrorMessage) { + return imageInputErrorMessage; + } else if (iconBuff && iconBuff.length > Globals.MAX_GUILD_ICON_SIZE) { + return `Icon is too large. (${ElementsUtil.humanSize(iconBuff.length)}>${ElementsUtil.humanSize(Globals.MAX_GUILD_ICON_SIZE)}) Try a 512x512 icon.`; + } else if (name.length === 0) { + return 'Name is empty'; + } else if (name.length > Globals.MAX_GUILD_NAME_LENGTH) { + return `Name is too long. (${name.length}>${Globals.MAX_GUILD_NAME_LENGTH} characters)`; + } else { + return null; + } + }, [ iconFailed, imageInputErrorMessage, iconBuff, name ]); + + const resetChanges = () => { + setImageInputErrorMessage(null); + setName(savedName); + setIconBuff(savedIconBuff); + } + + const saveChanges = useCallback(async () => { + if (errorMessage) return; + if (saving) return; + + setSaving(true); + + if (name !== savedName) { + // Save name + try { + await guild.requestSetGuildName(name); + setSavedName(name); + } catch (e: unknown) { + LOG.error('error setting guild name', e); + setSaveFailed(true); + setSaving(false); + return; + } + } + + if (iconBuff && iconBuff?.toString('hex') !== savedIconBuff?.toString('hex')) { + // Save icon + try { + LOG.debug('saving icon'); + await guild.requestSetGuildIcon(iconBuff); + setSavedIconBuff(iconBuff); + } catch (e: unknown) { + LOG.error('error setting guild icon', e); + setSaveFailed(true); + setSaving(false); + return; + } + } + + setSaving(false); + }, [ errorMessage, saving, name, savedName, iconBuff ]); + + return ( + +
+
+ +
+
+ +
+
+
+ ); +} + +export default GuildOverviewDisplay; diff --git a/src/client/webapp/elements/overlay-guild-settings.tsx b/src/client/webapp/elements/overlay-guild-settings.tsx index 0948f1c..63dcc42 100644 --- a/src/client/webapp/elements/overlay-guild-settings.tsx +++ b/src/client/webapp/elements/overlay-guild-settings.tsx @@ -107,7 +107,7 @@ export default function createGuildSettingsOverlay(document: Document, q: Q, gui }); BaseElements.bindImageUploadEvents(q.$$$(element, '.image-input-upload') as HTMLInputElement, { - maxSize: Globals.MAX_ICON_SIZE, + maxSize: Globals.MAX_GUILD_ICON_SIZE, acceptedMimeTypes: [ 'image/png', 'image/jpeg', 'image/jpg' ], onChangeStart: async () => await updatePopups(), onCleared: () => {}, diff --git a/src/client/webapp/elements/overlay-image.tsx b/src/client/webapp/elements/overlay-image.tsx deleted file mode 100644 index 5870fd3..0000000 --- a/src/client/webapp/elements/overlay-image.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as electronRemote from '@electron/remote'; -const electronConsole = electronRemote.getGlobal('console') as Console; -import Logger from '../../../logger/logger'; -const LOG = Logger.create(__filename, electronConsole); - -import * as FileType from 'file-type' - -import BaseElements, { HTMLElementWithRemoveSelf } from './require/base-elements'; -import ElementsUtil from './require/elements-util'; - -import Q from '../q-module'; -import createImageContextMenu from './context-menu-img'; -import CombinedGuild from '../guild-combined'; - -import React from 'react'; - -export default function createImageOverlay(document: Document, q: Q, guild: CombinedGuild, resourceId: string, resourceName: string): HTMLElementWithRemoveSelf { - const element = BaseElements.createOverlay(document, ( -
- {resourceName} -
-
-
{resourceName}
-
Loading Size...
-
-
Loading...
-
-
- )); - - (async () => { - try { - const resource = await guild.fetchResource(resourceId); - const src = await ElementsUtil.getImageBufferSrc(resource.data); - (q.$$$(element, '.content img') as HTMLImageElement).src = src; - q.$$$(element, '.download .size').innerText = ElementsUtil.humanSize(resource.data.length); - - const { mime, ext } = (await FileType.fromBuffer(resource.data)) ?? { mime: null, ext: null }; - if (mime === null || ext === null) throw new Error('unable to get mime/ext'); - - q.$$$(element, '.content img').addEventListener('contextmenu', (e) => { - const contextMenu = createImageContextMenu(document, q, guild, resourceName, resource.data, mime as string, ext as string, false); - document.body.appendChild(contextMenu); - const relativeTo = { x: e.pageX, y: e.pageY }; - ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'right' }); - }); - - q.$$$(element, '.button').innerText = 'Save'; - q.$$$(element, '.button').addEventListener('click', ElementsUtil.createDownloadListener({ - downloadBuff: resource.data, - resourceName: resourceName, - downloadStartFunc: () => { - q.$$$(element, '.button').innerText = 'Downloading...'; - }, - downloadFailFunc: async () => { - q.$$$(element, '.button').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button'), 400); - }, - writeStartFunc: () => { - q.$$$(element, '.button').innerText = 'Writing...'; - }, - writeFailFunc: async () => { - q.$$$(element, '.button').innerText = 'Try Again'; - await ElementsUtil.shakeElement(q.$$$(element, '.button'), 400); - }, - successFunc: (_downloadPath: string) => { - q.$$$(element, '.button').innerText = 'Reveal in Explorer'; - } - })); - } catch (e) { - LOG.error('error loading overlay image', e); - (q.$$$(element, '.content img') as HTMLImageElement).src = './img/error.png'; - q.$$$(element, '.download .size').innerText = 'err'; - q.$$$(element, '.button').innerText = 'Error'; - } - })(); - return element; -} diff --git a/src/client/webapp/elements/overlays/overlay-guild-settings.tsx b/src/client/webapp/elements/overlays/overlay-guild-settings.tsx new file mode 100644 index 0000000..1dca506 --- /dev/null +++ b/src/client/webapp/elements/overlays/overlay-guild-settings.tsx @@ -0,0 +1,27 @@ +import React, { FC, useState } from "react"; +import { GuildMetadata } from "../../data-types"; +import CombinedGuild from "../../guild-combined"; +import GuildOverviewDisplay from "../displays/display-guild-overview"; + +export interface GuildSettingsOverlayProps { + guild: CombinedGuild; + guildMeta: GuildMetadata; +} +const GuildSettingsOverlay: FC = (props: GuildSettingsOverlayProps) => { + const { guild, guildMeta } = props; + + return ( +
+
+
{guildMeta.name}
+
Overview
+
Channels
+
Roles
+
Invites
+
+ +
+ ); +} + +export default GuildSettingsOverlay; diff --git a/src/client/webapp/elements/overlays/overlay-image.tsx b/src/client/webapp/elements/overlays/overlay-image.tsx index cbac3c1..6311c48 100644 --- a/src/client/webapp/elements/overlays/overlay-image.tsx +++ b/src/client/webapp/elements/overlays/overlay-image.tsx @@ -47,7 +47,7 @@ const ImageOverlay: FC = (props: ImageOverlayProps) => { return; } })(); - }); + }, []); const onImageContextMenu = (e: React.MouseEvent) => { // TODO: This should be in react! diff --git a/src/client/webapp/globals.ts b/src/client/webapp/globals.ts index a50d833..a211f6a 100644 --- a/src/client/webapp/globals.ts +++ b/src/client/webapp/globals.ts @@ -13,7 +13,7 @@ export default class Globals { static MAX_AVATAR_SIZE = 1024 * 128; // 128 KB max avatar size static MAX_DISPLAY_NAME_LENGTH = 32; // 32 char max display name length - static MAX_ICON_SIZE = 1024 * 128; // 128 KB max guild icon size + static MAX_GUILD_ICON_SIZE = 1024 * 128; // 128 KB max guild icon size static MAX_GUILD_NAME_LENGTH = 64; // 64 char max guild name length static MAX_CHANNEL_NAME_LENGTH = 32; // 32 char max channel name length diff --git a/src/client/webapp/styles/components.scss b/src/client/webapp/styles/components.scss new file mode 100644 index 0000000..4c8bcc5 --- /dev/null +++ b/src/client/webapp/styles/components.scss @@ -0,0 +1,116 @@ +@import "theme.scss"; + +.text-input-react { + .label { + font-size: 0.75em; + font-weight: bold; + color: $interactive-normal; + text-transform: uppercase; + margin-bottom: 2px; + } + + input { + font-size: inherit; + font-family: inherit; + color: $text-normal; + background-color: $background-input; + border: 1px solid $border-input; + border-radius: 3px; + max-height: 100px; + overflow-y: scroll; + padding: 8px; + + &:hover { + border-color: $border-input-hover; + } + + &:focus { + outline: none; + border-color: $border-input-focus; + } + } +} + +.image-edit-input-react { + position: relative; + + label { + cursor: pointer; + } + + img.value { + width: 64px; + height: 64px; + border-radius: 8px; + } + + .modify { + position: absolute; + background-color: $brand; + padding: 6px; + border-radius: 16px; + right: -4px; + bottom: -4px; + + img.pencil { + display: block; + width: 10px; + height: 10px; + } + } + + input { + display: none; + } +} + +.display { + $content-border-radius: 4px; + + flex: 1; + border-top-right-radius: $content-border-radius; + border-bottom-right-radius: $content-border-radius; + background-color: $background-primary; + position: relative; + + > .scroll { + margin: 32px; + } + + > .popup { + position: absolute; + bottom: 0; + left: 0; + box-sizing: border-box; + width: 100%; + + .content { + margin: 16px; + padding: 12px 12px 12px 16px; + display: flex; + justify-content: space-between; + align-items: center; + color: $interactive-active; + background-color: $background-popup-message; + border-radius: 8px; + } + + .tip { + font-weight: 500; + } + + .actions { + display: flex; + + .button { + padding: 8px 12px; + font-size: 0.85em; + font-weight: bold; + } + + :not(:last-child) { + margin-right: 4px; + } + } + } +} \ No newline at end of file diff --git a/src/client/webapp/styles/overlays.scss b/src/client/webapp/styles/overlays.scss index 33cfa6e..ba4c6dc 100644 --- a/src/client/webapp/styles/overlays.scss +++ b/src/client/webapp/styles/overlays.scss @@ -16,7 +16,7 @@ body > .overlay, background-color: $background-overlay; /* General Controls */ - input[type=text], .text-input { + .text-input { font-size: inherit; font-family: inherit; color: $text-normal; @@ -37,14 +37,6 @@ body > .overlay, } } - .label { - font-size: 0.75em; - font-weight: bold; - color: $interactive-normal; - text-transform: uppercase; - margin-bottom: 2px; - } - /* Popup Image */ > .content.popup-image { @@ -370,59 +362,6 @@ body > .overlay, } } } - - > .display { - flex: 1; - border-top-right-radius: $content-border-radius; - border-bottom-right-radius: $content-border-radius; - background-color: $background-primary; - position: relative; - - > .scroll { - margin: 32px; - } - - > .popup { - position: absolute; - bottom: 0; - left: 0; - box-sizing: border-box; - width: 100%; - - &:not(.enabled) { - display: none; - } - - .content { - margin: 16px; - padding: 12px 12px 12px 16px; - display: flex; - justify-content: space-between; - align-items: center; - color: $interactive-active; - background-color: $background-popup-message; - border-radius: 8px; - } - - .tip { - font-weight: 500; - } - - .actions { - display: flex; - - .button { - padding: 8px 12px; - font-size: 0.85em; - font-weight: bold; - } - - :not(:last-child) { - margin-right: 4px; - } - } - } - } } /* guild Settings Overlay */ @@ -434,35 +373,6 @@ body > .overlay, display: flex; margin-bottom: 12px; - .image-input-label { - cursor: pointer; - } - - .icon { - position: relative; - - > img { - width: 64px; - height: 64px; - border-radius: 8px; - } - - .modify { - position: absolute; - background-color: $brand; - padding: 6px; - border-radius: 16px; - right: -4px; - bottom: -4px; - - > img { - display: block; - width: 10px; - height: 10px; - } - } - } - .name { margin-left: 16px; } diff --git a/src/client/webapp/styles/styles.scss b/src/client/webapp/styles/styles.scss index c2fcab4..95ed443 100644 --- a/src/client/webapp/styles/styles.scss +++ b/src/client/webapp/styles/styles.scss @@ -1,6 +1,8 @@ @import "theme.scss"; @import "fonts.scss"; +@import "components.scss"; + @import "buttons.scss"; @import "channel-feed.scss"; @import "channel-list.scss";