Refactor stuff from common.css to CSS Modules

This commit is contained in:
Kalle 2025-12-30 17:15:19 +02:00
parent 46814e86cc
commit 3977dccd64
52 changed files with 1223 additions and 1315 deletions

View File

@ -0,0 +1,56 @@
.alert {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
border-radius: var(--rounded);
background-color: var(--color-info-low);
color: var(--color-info-high);
font-size: var(--fonts-sm);
font-weight: var(--semi-bold);
gap: var(--s-2);
margin-inline: auto;
padding-block: var(--s-1-5);
padding-inline: var(--s-3) var(--s-4);
text-align: center;
& > svg {
height: 1.75rem;
fill: var(--color-info);
}
}
.tiny {
font-size: var(--fonts-xs);
& > svg {
height: 1.25rem;
}
}
.warning {
background-color: var(--color-warning-low);
color: var(--color-warning-high);
& > svg {
fill: var(--color-warning);
}
}
.error {
background-color: var(--color-error-low);
color: var(--color-error-high);
& > svg {
fill: var(--color-error);
}
}
.success {
background-color: var(--color-success-low);
color: var(--color-success-high);
& > svg {
fill: var(--color-success);
}
}

View File

@ -1,6 +1,7 @@
import clsx from "clsx";
import type * as React from "react";
import { assertUnreachable } from "~/utils/types";
import styles from "./Alert.module.css";
import { AlertIcon } from "./icons/Alert";
import { CheckmarkIcon } from "./icons/Checkmark";
import { ErrorIcon } from "./icons/Error";
@ -22,11 +23,11 @@ export function Alert({
}) {
return (
<div
className={clsx("alert", alertClassName, {
tiny,
warning: variation === "WARNING",
error: variation === "ERROR",
success: variation === "SUCCESS",
className={clsx(styles.alert, alertClassName, {
[styles.tiny]: tiny,
[styles.warning]: variation === "WARNING",
[styles.error]: variation === "ERROR",
[styles.success]: variation === "SUCCESS",
})}
>
<Icon variation={variation} />{" "}

View File

@ -0,0 +1,4 @@
.avatar {
border-radius: 50%;
background-color: var(--color-bg-higher);
}

View File

@ -2,6 +2,7 @@ import clsx from "clsx";
import * as React from "react";
import type { Tables } from "~/db/tables";
import { BLANK_IMAGE_URL, discordAvatarUrl } from "~/utils/urls";
import styles from "./Avatar.module.css";
const dimensions = {
xxxs: 16,
@ -49,7 +50,7 @@ export function Avatar({
return (
<img
className={clsx("avatar", className)}
className={clsx(styles.avatar, className)}
src={src}
alt={alt}
title={alt ? alt : undefined}

View File

@ -0,0 +1,34 @@
.container {
height: var(--chart-height, 175px);
width: var(--chart-width);
background-color: var(--chart-bg, var(--color-bg-high));
border-radius: var(--rounded);
}
.tooltip {
border: 1.75px solid var(--color-border);
border-radius: var(--rounded);
background-color: var(--color-bg);
padding: var(--s-1) var(--s-2);
font-weight: var(--semi-bold);
font-size: var(--fonts-sm);
display: flex;
flex-direction: column;
gap: var(--s-1);
}
.tooltipValue {
margin-inline-start: auto;
min-width: 40px;
}
.dot {
background-color: var(--dot-color);
border-radius: 100%;
width: 12px;
height: 12px;
}
.dotFocused {
outline: 3px solid var(--dot-color-outline);
}

View File

@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
import { Theme, useTheme } from "~/features/theme/core/provider";
import { useIsMounted } from "~/hooks/useIsMounted";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import styles from "./Chart.module.css";
export default function Chart({
options,
@ -62,11 +63,11 @@ export default function Chart({
);
if (!isMounted) {
return <div className={clsx("chart__container", containerClassName)} />;
return <div className={clsx(styles.container, containerClassName)} />;
}
return (
<div className={clsx("chart__container", containerClassName)}>
<div className={clsx(styles.container, containerClassName)}>
<ReactChart
options={{
data: options,
@ -124,7 +125,7 @@ function ChartTooltip({
};
return (
<div className="chart__tooltip">
<div className={styles.tooltip}>
<h3 className="text-center text-md">
{header()}
{headerSuffix}
@ -135,8 +136,8 @@ function ChartTooltip({
return (
<div key={index} className="stack horizontal items-center sm">
<div
className={clsx("chart__dot", {
chart__dot__focused:
className={clsx(styles.dot, {
[styles.dotFocused]:
focusedDatum?.seriesId === dataPoint.seriesId,
})}
style={{
@ -144,10 +145,8 @@ function ChartTooltip({
"--dot-color-outline": color.replace(")", "-transparent)"),
}}
/>
<div className="chart__tooltip__label">
{dataPoint.originalSeries.label}
</div>
<div className="chart__tooltip__value">
<div>{dataPoint.originalSeries.label}</div>
<div className={styles.tooltipValue}>
{dataPoint.secondaryValue}
{valueSuffix}
</div>

View File

@ -0,0 +1,80 @@
.summary {
padding: var(--s-2) var(--s-3);
border: 2px solid var(--color-border);
border-radius: var(--rounded-sm);
background-color: var(--color-bg);
font-weight: var(--bold);
font-size: var(--fonts-xs);
& div {
display: inline-flex;
}
& svg {
width: 24px;
color: var(--color-accent);
position: absolute;
right: 20px;
top: 14px;
}
& + div {
margin-block-start: var(--s-4);
}
}
.container {
width: 100%;
font-size: var(--fonts-sm);
font-weight: var(--bold);
padding: var(--s-3);
border: 2px solid var(--color-border);
border-radius: var(--rounded-sm);
background-color: var(--color-bg);
margin-bottom: var(--s-3);
overflow-x: auto;
}
.grid {
display: grid;
justify-content: space-between;
grid-template-columns: repeat(3, max-content);
gap: var(--s-3);
}
.table {
width: 100%;
border-spacing: 0;
text-indent: 0;
text-align: left;
font-size: var(--fonts-xs);
& svg {
width: 1rem;
height: 1rem;
display: inline;
vertical-align: sub;
margin-right: 2px;
}
& td {
padding-block: var(--s-2);
}
& tr:last-child td {
border-bottom: none;
padding-bottom: 0;
}
}
.contrast {
text-wrap-mode: nowrap;
}
.fail {
color: var(--color-error);
}
.success {
color: var(--color-success);
}

View File

@ -3,6 +3,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { useDebounce } from "react-use";
import { CUSTOM_CSS_VAR_COLORS } from "~/features/user-page/user-page-constants";
import styles from "./CustomizedColorsInput.module.css";
import { SendouButton } from "./elements/Button";
import { InfoPopover } from "./InfoPopover";
import { AlertIcon } from "./icons/Alert";
@ -72,7 +73,7 @@ export function CustomizedColorsInput({
return (
<details className="w-full">
<summary className="colors__summary">
<summary className={styles.summary}>
<div>
<span>{t("custom.colors.title")}</span>
</div>
@ -86,7 +87,7 @@ export function CustomizedColorsInput({
colorsWithDefaultsFilteredOut(colors, defaultColors),
)}
/>
<div className="colors__container colors__grid">
<div className={clsx(styles.container, styles.grid)}>
{CUSTOM_CSS_VAR_COLORS.filter(
(cssVar) => cssVar !== "bg-lightest",
).map((cssVar) => {
@ -138,12 +139,10 @@ export function CustomizedColorsInput({
<Label labelClassName="stack horizontal sm items-center">
{t("custom.colors.contrast.title")}
<InfoPopover tiny>
<div className="colors__description">
{t("custom.colors.contrast.description")}
</div>
<div>{t("custom.colors.contrast.description")}</div>
</InfoPopover>
</Label>
<table className="colors__container colors__table">
<table className={clsx(styles.container, styles.table)}>
<thead>
<tr>
<th>{t("custom.colors.contrast.first-color")}</th>
@ -160,8 +159,10 @@ export function CustomizedColorsInput({
<td>{t(`custom.colors.${contrast.colors[1]}`)}</td>
<td
className={clsx(
"colors__contrast",
contrast.contrast.AA.failed ? "fail" : "success",
styles.contrast,
contrast.contrast.AA.failed
? styles.fail
: styles.success,
)}
>
{contrast.contrast.AA.failed ? (
@ -173,8 +174,10 @@ export function CustomizedColorsInput({
</td>
<td
className={clsx(
"colors__contrast",
contrast.contrast.AAA.failed ? "fail" : "success",
styles.contrast,
contrast.contrast.AAA.failed
? styles.fail
: styles.success,
)}
>
{contrast.contrast.AAA.failed ? (

View File

@ -0,0 +1,28 @@
.divider {
display: flex;
width: 100%;
align-items: center;
color: var(--color-text-accent);
font-size: var(--fonts-lg);
text-align: center;
&::before,
&::after {
flex: 1;
min-width: 1rem;
border-bottom: 2px solid var(--color-bg-high);
content: "";
}
&:not(:empty)::before {
margin-right: 0.25em;
}
&:not(:empty)::after {
margin-left: 0.25em;
}
}
.smallText {
font-size: var(--fonts-sm);
}

View File

@ -1,4 +1,5 @@
import clsx from "clsx";
import styles from "./Divider.module.css";
export function Divider({
children,
@ -10,7 +11,11 @@ export function Divider({
smallText?: boolean;
}) {
return (
<div className={clsx("divider", className, { "text-sm": smallText })}>
<div
className={clsx(styles.divider, className, {
[styles.smallText]: smallText,
})}
>
{children}
</div>
);

View File

@ -0,0 +1,7 @@
.container {
font-size: var(--fonts-sm);
& > h4 {
color: var(--color-error);
}
}

View File

@ -1,6 +1,7 @@
import { useActionData } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import type { Namespace } from "~/modules/i18n/resources.server";
import styles from "./FormErrors.module.css";
export function FormErrors({ namespace }: { namespace: Namespace }) {
const { t } = useTranslation(["common", namespace]);
@ -11,7 +12,7 @@ export function FormErrors({ namespace }: { namespace: Namespace }) {
}
return (
<div className="form-errors">
<div className={styles.container}>
<h4>{t("common:forms.errors.title")}:</h4>
<ol>
{actionData.errors.map((error) => (

View File

@ -0,0 +1,13 @@
.error {
display: block;
color: var(--color-error);
font-size: var(--fonts-xs);
margin-block-start: var(--label-margin);
}
.info {
display: block;
color: var(--color-text-high);
font-size: var(--fonts-xs);
margin-block-start: var(--label-margin);
}

View File

@ -1,5 +1,6 @@
import clsx from "clsx";
import type * as React from "react";
import styles from "./FormMessage.module.css";
export function FormMessage({
children,
@ -13,7 +14,7 @@ export function FormMessage({
return (
<div
className={clsx(
{ "info-message": type === "info", "error-message": type === "error" },
{ [styles.info]: type === "info", [styles.error]: type === "error" },
className,
)}
>

View File

@ -0,0 +1,8 @@
.tierContainer {
display: grid;
}
.tierImg {
grid-column: 1;
grid-row: 1;
}

View File

@ -19,6 +19,7 @@ import {
TIER_PLUS_URL,
tierImageUrl,
} from "~/utils/urls";
import styles from "./Image.module.css";
interface ImageProps {
path: string;
@ -229,14 +230,14 @@ export function TierImage({ tier, className, width = 200 }: TierImageProps) {
const height = width * 0.8675;
return (
<div className={clsx("tier__container", className)} style={{ width }}>
<div className={clsx(styles.tierContainer, className)} style={{ width }}>
<Image
path={tierImageUrl(tier.name)}
width={width}
height={height}
alt={title}
title={title}
containerClassName="tier__img"
containerClassName={styles.tierImg}
/>
{tier.isPlus ? (
<Image
@ -245,7 +246,7 @@ export function TierImage({ tier, className, width = 200 }: TierImageProps) {
height={height}
alt={title}
title={title}
containerClassName="tier__img"
containerClassName={styles.tierImg}
/>
) : null}
</div>

View File

@ -0,0 +1,24 @@
.trigger {
border: 2px solid var(--color-bg-higher);
border-radius: 100%;
background-color: transparent;
color: var(--color-text);
font-size: var(--fonts-md);
padding: var(--s-0-5);
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
&:focus-visible {
outline: var(--input-focus-ring);
outline-offset: 1px;
}
}
.triggerTiny {
width: 20px;
height: 20px;
font-size: var(--fonts-xs);
}

View File

@ -1,6 +1,7 @@
import clsx from "clsx";
import { Button } from "react-aria-components";
import { SendouPopover } from "./elements/Popover";
import styles from "./InfoPopover.module.css";
export function InfoPopover({
children,
@ -15,14 +16,9 @@ export function InfoPopover({
<SendouPopover
trigger={
<Button
className={clsx(
"react-aria-Button",
"info-popover__trigger",
className,
{
"info-popover__trigger__tiny": tiny,
},
)}
className={clsx(styles.trigger, className, {
[styles.triggerTiny]: tiny,
})}
>
?
</Button>

View File

@ -0,0 +1,60 @@
.container {
display: flex;
font-size: var(--fonts-sm);
outline: none;
line-height: var(--input-line-height);
border: 2px solid var(--color-border);
border-radius: var(--rounded-sm);
background-color: var(--color-bg);
height: var(--input-height);
width: 100%;
& svg {
color: var(--color-text-high);
height: calc(var(--input-height) / 2);
margin: auto;
margin-right: 15px;
}
&:has(input:user-invalid) {
outline: var(--input-focus-ring-error);
outline-offset: 1px;
}
&:focus-within {
outline: var(--input-focus-ring);
outline-offset: 1px;
}
input {
border-radius: var(--rounded-sm);
padding: 0 var(--input-padding-inline);
outline: none;
width: 100%;
background-color: inherit;
border: none;
&::placeholder {
color: var(--color-text-high);
}
}
}
.readOnly {
pointer-events: none;
cursor: not-allowed;
opacity: 0.5;
outline: none;
}
.addon {
display: grid;
border-radius: var(--rounded-xs) 0 0 var(--rounded-xs);
background-color: var(--color-bg-high);
color: var(--color-text-high);
font-size: var(--fonts-xs);
font-weight: var(--semi-bold);
padding-inline: var(--s-2);
place-items: center;
white-space: nowrap;
}

View File

@ -1,4 +1,5 @@
import clsx from "clsx";
import styles from "./Input.module.css";
export function Input({
name,
@ -47,11 +48,11 @@ export function Input({
}) {
return (
<div
className={clsx("input-container", className, {
"input__read-only": readOnly,
className={clsx(styles.container, className, {
[styles.readOnly]: readOnly,
})}
>
{leftAddon ? <div className="input-addon">{leftAddon}</div> : null}
{leftAddon ? <div className={styles.addon}>{leftAddon}</div> : null}
<input
className="in-container"
name={name}

View File

@ -0,0 +1,24 @@
.container {
display: flex;
align-items: flex-end;
gap: var(--s-2);
margin-block-end: var(--label-margin);
& > label {
margin: 0;
}
}
.value {
color: var(--color-text-high);
font-size: var(--fonts-xxs);
margin-block-start: -5px;
}
.valueWarning {
color: var(--color-warning);
}
.valueError {
color: var(--color-error);
}

View File

@ -1,4 +1,5 @@
import clsx from "clsx";
import styles from "./Label.module.css";
type LabelProps = Pick<
React.DetailedHTMLProps<
@ -27,12 +28,12 @@ export function Label({
spaced = true,
}: LabelProps) {
return (
<div className={clsx("label__container", className, { "mb-0": !spaced })}>
<div className={clsx(styles.container, className, { "mb-0": !spaced })}>
<label htmlFor={htmlFor} className={labelClassName}>
{children} {required && <span className="text-error">*</span>}
</label>
{valueLimits ? (
<div className={clsx("label__value", lengthWarning(valueLimits))}>
<div className={clsx(styles.value, lengthWarning(valueLimits, styles))}>
{valueLimits.current}/{valueLimits.max}
</div>
) : null}
@ -40,9 +41,12 @@ export function Label({
);
}
function lengthWarning(valueLimits: NonNullable<LabelProps["valueLimits"]>) {
if (valueLimits.current > valueLimits.max) return "error";
if (valueLimits.current / valueLimits.max >= 0.9) return "warning";
function lengthWarning(
valueLimits: NonNullable<LabelProps["valueLimits"]>,
s: typeof styles,
) {
if (valueLimits.current > valueLimits.max) return s.valueError;
if (valueLimits.current / valueLimits.max >= 0.9) return s.valueWarning;
return;
}

View File

@ -0,0 +1,44 @@
.container {
display: grid;
grid-template-columns: auto auto auto;
gap: var(--s-2);
align-items: center;
justify-items: center;
justify-content: center;
}
.dots {
display: none;
align-items: center;
justify-content: center;
gap: var(--s-1);
flex-wrap: wrap;
}
.dot {
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
background-color: var(--color-bg-higher);
transition: all 0.2s ease;
}
.dotActive {
background-color: var(--color-text-accent);
}
.pageCount {
font-size: var(--fonts-sm);
font-weight: var(--bold);
color: var(--color-accent);
}
@media screen and (min-width: 640px) {
.dots {
display: flex;
}
.pageCount {
display: none;
}
}

View File

@ -3,6 +3,7 @@ import { SendouButton } from "~/components/elements/Button";
import { ArrowLeftIcon } from "~/components/icons/ArrowLeft";
import { ArrowRightIcon } from "~/components/icons/ArrowRight";
import { nullFilledArray } from "~/utils/arrays";
import styles from "./Pagination.module.css";
export function Pagination({
currentPage,
@ -18,7 +19,7 @@ export function Pagination({
setPage: (page: number) => void;
}) {
return (
<div className="pagination__container">
<div className={styles.container}>
<SendouButton
icon={<ArrowLeftIcon />}
variant="outlined"
@ -27,19 +28,19 @@ export function Pagination({
onPress={previousPage}
aria-label="Previous page"
/>
<div className="pagination__dots">
<div className={styles.dots}>
{nullFilledArray(pagesCount).map((_, i) => (
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
<div
key={i}
className={clsx("pagination__dot", {
pagination__dot__active: i === currentPage - 1,
className={clsx(styles.dot, {
[styles.dotActive]: i === currentPage - 1,
})}
onClick={() => setPage(i + 1)}
/>
))}
</div>
<div className="pagination__page-count">
<div className={styles.pageCount}>
{currentPage}/{pagesCount}
</div>
<SendouButton

View File

@ -0,0 +1,8 @@
.input {
position: absolute;
width: 0;
height: 0;
border: none;
opacity: 0;
pointer-events: none;
}

View File

@ -1,3 +1,5 @@
import styles from "./RequiredHiddenInput.module.css";
export function RequiredHiddenInput({
value,
isValid,
@ -9,7 +11,7 @@ export function RequiredHiddenInput({
}) {
return (
<input
className="hidden-input-with-validation"
className={styles.input}
name={name}
value={isValid ? value : []}
// empty onChange is because otherwise it will give a React error in console

View File

@ -0,0 +1,12 @@
.section {
& > div {
padding: var(--s-2);
border-radius: var(--rounded);
background-color: var(--color-bg);
}
& > h2 {
color: var(--color-text-high);
font-size: var(--fonts-md);
}
}

View File

@ -1,3 +1,5 @@
import styles from "./Section.module.css";
export function Section({
title,
children,
@ -8,7 +10,7 @@ export function Section({
className?: string;
}) {
return (
<section className="section">
<section className={styles.section}>
{title && <h2>{title}</h2>}
<div className={className}>{children}</div>
</section>

View File

@ -0,0 +1,63 @@
.container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--s-4);
margin-block-end: var(--s-8);
}
.secondary {
margin-block-end: var(--s-2);
}
.linkContainer {
display: flex;
max-width: 110px;
flex: 1;
flex-direction: column;
align-items: center;
color: var(--color-text);
gap: var(--s-1-5);
}
.linkContainer.active {
color: var(--color-text-accent);
}
.link {
width: 100%;
padding: var(--s-1) var(--s-4);
border-radius: var(--rounded);
background-color: var(--color-bg-high);
cursor: pointer;
font-size: var(--fonts-xs);
text-align: center;
white-space: nowrap;
}
.linkSecondary {
font-size: var(--fonts-xxs);
padding: var(--s-0-5) var(--s-2);
background-color: var(--color-bg-high);
}
.container.compact .link {
padding: var(--s-1) var(--s-2);
}
.borderGuy {
width: 78%;
height: 3px;
border-radius: var(--rounded);
background-color: var(--color-bg-higher);
visibility: hidden;
}
.borderGuySecondary {
height: 2.5px;
background-color: var(--color-bg-high);
}
.linkContainer.active > .borderGuy {
visibility: initial;
}

View File

@ -2,6 +2,7 @@ import type { LinkProps } from "@remix-run/react";
import { NavLink } from "@remix-run/react";
import clsx from "clsx";
import type * as React from "react";
import styles from "./SubNav.module.css";
export function SubNav({
children,
@ -13,8 +14,8 @@ export function SubNav({
return (
<div>
<nav
className={clsx("sub-nav__container", {
"sub-nav__container__secondary": secondary,
className={clsx(styles.container, {
[styles.secondary]: secondary,
})}
>
{children}
@ -41,8 +42,8 @@ export function SubNavLink({
return (
<NavLink
className={(state) =>
clsx("sub-nav__link__container", {
active: controlled ? active : state.isActive,
clsx(styles.linkContainer, {
[styles.active]: controlled ? active : state.isActive,
pending: state.isPending,
})
}
@ -50,15 +51,15 @@ export function SubNavLink({
{...props}
>
<div
className={clsx("sub-nav__link", className, {
"sub-nav__link__secondary": secondary,
className={clsx(styles.link, className, {
[styles.linkSecondary]: secondary,
})}
>
{children}
</div>
<div
className={clsx("sub-nav__border-guy", {
"sub-nav__border-guy__secondary": secondary,
className={clsx(styles.borderGuy, {
[styles.borderGuySecondary]: secondary,
})}
/>
</NavLink>

View File

@ -0,0 +1,38 @@
.container {
position: relative;
width: 100%;
overflow: auto;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: var(--fonts-xs);
text-align: left;
border-color: var(--color-border);
& > thead {
font-size: var(--fonts-xxs);
}
& tbody tr:hover {
background-color: var(--color-bg-high);
}
& > thead > tr > th {
padding: var(--s-2);
}
& > tbody > tr > td {
padding: var(--s-2) var(--s-2-5);
}
& tr:first-child td {
border-top: 1px solid var(--color-border);
}
& td {
border-bottom: 1px solid var(--color-border);
}
}

View File

@ -1,7 +1,9 @@
import styles from "./Table.module.css";
export function Table({ children }: { children: React.ReactNode }) {
return (
<div className="my-table__container">
<table className="my-table">{children}</table>
<div className={styles.container}>
<table className={styles.table}>{children}</table>
</div>
);
}

View File

@ -0,0 +1,14 @@
.textOnlyButton {
cursor: pointer;
border: 0;
background-color: inherit;
color: inherit;
margin: 0;
padding: 0;
}
.dotted {
text-decoration-style: dotted;
text-decoration-line: underline;
text-decoration-thickness: 2px;
}

View File

@ -9,6 +9,7 @@ import { SendouButton } from "./elements/Button";
import popoverStyles from "./elements/Popover.module.css";
import { CheckmarkIcon } from "./icons/Checkmark";
import { ClipboardIcon } from "./icons/Clipboard";
import styles from "./TimePopover.module.css";
export default function TimePopover({
time,
@ -55,8 +56,9 @@ export default function TimePopover({
ref={triggerRef}
className={clsx(
className,
"clickable text-only-button",
underline ? "dotted" : "",
"clickable",
styles.textOnlyButton,
underline ? styles.dotted : "",
)}
onClick={() => {
setOpen(true);

View File

@ -0,0 +1,18 @@
.container {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%;
}
.containerApi {
width: fit-content;
}
.iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

View File

@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { toastQueue } from "./elements/Toast";
import styles from "./YouTubeEmbed.module.css";
export function YouTubeEmbed({
id,
@ -117,16 +118,16 @@ export function YouTubeEmbed({
}
return (
<div className="youtube__container--api">
<div className={styles.containerApi}>
<div ref={containerRef} />
</div>
);
}
return (
<div className="youtube__container">
<div className={styles.container}>
<iframe
className="youtube__iframe"
className={styles.iframe}
src={`https://www.youtube.com/embed/${id}?autoplay=${
autoplay ? "1" : "0"
}&controls=1&rel=0&modestbranding=1&start=${start ?? 0}`}

View File

@ -0,0 +1,81 @@
.dialogImgContainer {
width: 100vw;
height: 90vh;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
padding: 0;
overflow: visible;
flex-direction: column;
&:focus-visible {
outline: none;
}
}
.thumbnail {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
scale: 1.025;
}
}
.dialogTag {
background-color: #fff;
border-radius: var(--rounded);
color: #000;
font-size: var(--fonts-xxs);
padding-inline: var(--s-1);
margin-block: var(--s-1) var(--s-0-5);
}
.dialogTagUser {
background-color: var(--color-accent);
}
.dialogDescription {
font-size: var(--fonts-sm);
text-align: center;
color: #fff;
}
.dialogImg {
max-width: 100%;
max-height: 75vh;
width: 100%;
height: auto;
object-fit: contain;
display: block;
}
.dialogImgContainer img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.commsHeader {
font-weight: var(--bold);
color: var(--color-accent);
font-size: var(--fonts-sm);
}
.deleteTagButton {
margin-block-start: -5px;
margin-inline-start: 1px;
}
.creationModeSwitcherContainer {
height: 20px;
}
.tagsContainer {
display: flex;
gap: var(--s-0-5) var(--s-2);
justify-content: center;
flex-wrap: wrap;
margin-block-start: var(--s-0-5);
}

View File

@ -21,6 +21,7 @@ import { ResponsiveMasonry } from "../../../modules/responsive-masonry/component
import { ART_PER_PAGE } from "../art-constants";
import type { ListedArt } from "../art-types";
import { previewUrl } from "../art-utils";
import styles from "./ArtGrid.module.css";
export function ArtGrid({
arts,
@ -107,18 +108,18 @@ function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) {
alt=""
src={art.url}
loading="lazy"
className="art__dialog__img"
className={styles.dialogImg}
onLoad={() => setImageLoaded(true)}
/>
{art.tags || art.linkedUsers ? (
<div
className={clsx("art__tags-container", { invisible: !imageLoaded })}
className={clsx(styles.tagsContainer, { invisible: !imageLoaded })}
>
{art.linkedUsers?.map((user) => (
<Link
to={userPage(user)}
key={user.discordId}
className="art__dialog__tag art__dialog__tag__user"
className={clsx(styles.dialogTag, styles.dialogTagUser)}
>
{user.username}
</Link>
@ -127,7 +128,7 @@ function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) {
<Link
to={artPage(tag.name)}
key={tag.id}
className="art__dialog__tag"
className={styles.dialogTag}
>
#{tag.name}
</Link>
@ -136,7 +137,7 @@ function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) {
) : null}
{art.description ? (
<div
className={clsx("art__dialog__description", {
className={clsx(styles.dialogDescription, {
invisible: !imageLoaded,
})}
>
@ -180,7 +181,7 @@ function ImagePreview({
loading="lazy"
onClick={onClick}
onLoad={() => setImageLoaded(true)}
className={enablePreview ? "art__thumbnail" : undefined}
className={enablePreview ? styles.thumbnail : undefined}
/>
);

View File

@ -0,0 +1,12 @@
.list {
display: flex;
flex-direction: column;
padding: 0;
gap: var(--s-6);
list-style: none;
}
.title {
color: var(--color-text-accent);
font-size: var(--fonts-md);
}

View File

@ -5,8 +5,8 @@ import { Main } from "~/components/Main";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { ARTICLES_MAIN_PAGE, articlePage, navIconUrl } from "~/utils/urls";
import { metaTags } from "../../../utils/remix";
import { loader } from "../loaders/a.server";
import styles from "./a.module.css";
export { loader };
export const handle: SendouRouteHandle = {
@ -33,13 +33,10 @@ export default function ArticlesMainPage() {
return (
<Main className="stack lg">
<ul className="articles-list">
<ul className={styles.list}>
{data.articles.map((article) => (
<li key={article.title}>
<Link
to={articlePage(article.slug)}
className="articles-list__title"
>
<Link to={articlePage(article.slug)} className={styles.title}>
{article.title}
</Link>
<div className="text-xs text-lighter">

View File

@ -0,0 +1,49 @@
.tags {
display: flex;
max-width: var(--tags-max-width, 18rem);
flex-wrap: wrap;
padding: 0;
font-size: var(--fonts-xxs);
font-weight: var(--semi-bold);
gap: var(--s-1);
list-style: none;
}
.small {
font-size: var(--fonts-xxxs);
& > li {
padding: 0 var(--s-1);
}
}
.centered {
justify-content: center;
}
.tags > li {
display: flex;
align-items: center;
border-radius: var(--rounded);
padding-inline: var(--s-1-5);
min-height: 20px;
}
.tag {
color: var(--color-text-inverse);
}
.tagDeleteButton {
margin-left: auto;
& > svg {
width: 0.85rem !important;
color: var(--color-text-inverse);
margin-inline: var(--s-1) 0 !important;
}
}
.tagBadges {
display: flex;
margin-inline-start: var(--s-1);
}

View File

@ -5,6 +5,7 @@ import { SendouButton } from "~/components/elements/Button";
import { CrossIcon } from "~/components/icons/Cross";
import type { CalendarEventTag } from "~/db/tables";
import { tags as allTags } from "../calendar-constants";
import styles from "./Tags.module.css";
export function Tags({
tags,
@ -24,24 +25,29 @@ export function Tags({
if (tags.length === 0) return null;
return (
<ul className={clsx("calendar__event__tags", { small, centered })}>
<ul
className={clsx(styles.tags, {
[styles.small]: small,
[styles.centered]: centered,
})}
>
{tags.map((tag) => (
<React.Fragment key={tag}>
<li
style={{ backgroundColor: allTags[tag].color }}
className="calendar__event__tag"
className={styles.tag}
>
{t(`tag.name.${tag}`)}
{onDelete && (
{onDelete ? (
<SendouButton
onPress={() => onDelete(tag)}
className="calendar__event__tag-delete-button"
className={styles.tagDeleteButton}
icon={<CrossIcon />}
variant="minimal"
aria-label="Remove date"
size="small"
/>
)}
) : null}
</li>
</React.Fragment>
))}

View File

@ -0,0 +1,110 @@
.container {
display: flex;
flex-direction: column;
}
.messages {
border: 2px solid var(--color-bg-high);
border-radius: 0 0 var(--rounded) var(--rounded);
padding: var(--s-2-5) 0 0 0;
display: flex;
flex-direction: column;
gap: var(--s-2);
height: 310px;
overflow-y: auto;
}
.message {
list-style: none;
display: flex;
gap: var(--s-2-5);
}
.messageUser {
font-weight: var(--semi-bold);
font-size: var(--fonts-sm);
color: var(--chat-user-color);
max-width: 110px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.messageTime {
font-size: var(--fonts-xxs);
color: var(--color-text-high);
margin-block-start: 3px;
}
.inputContainer {
margin-top: auto;
position: relative;
--input-width: 100%;
}
.messageContents {
font-size: var(--fonts-sm);
word-break: break-word;
}
.messageContentsPending {
opacity: 0.7;
}
.roomButton {
border: 0;
background-color: transparent;
color: var(--color-text-high);
border-radius: var(--rounded) var(--rounded) 0 0;
padding: var(--s-1) var(--s-1);
border-color: var(--color-bg-high);
font-size: var(--fonts-xs);
padding-block: var(--s-1);
padding-inline: var(--s-2);
display: flex;
width: auto;
align-items: center;
justify-content: center;
font-weight: var(--bold);
flex: 1 1 0px;
}
.roomButtonCurrent {
background-color: var(--color-bg-high);
color: var(--color-text);
}
.roomButtonUnseen {
color: var(--color-accent);
text-shadow: var(--fonts-xxxs);
margin-inline-start: var(--s-1);
width: 25px;
text-align: left;
}
.bottomRow {
display: flex;
justify-content: space-between;
align-items: center;
margin-block-start: var(--s-2);
}
.unseenMessages {
position: absolute;
font-size: var(--fonts-xxs);
font-weight: var(--bold);
border-radius: var(--rounded-sm);
background-color: var(--color-bg-higher);
border: none;
color: var(--color-text);
bottom: 60px;
right: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 25px;
width: max-content;
&:active {
transform: translate(-50%, -50%);
}
}

View File

@ -10,6 +10,7 @@ import { useTimeFormat } from "../../../hooks/useTimeFormat";
import { MESSAGE_MAX_LENGTH } from "../chat-constants";
import { useChat, useChatAutoScroll } from "../chat-hooks";
import type { ChatMessage, ChatProps, ChatUser } from "../chat-types";
import styles from "./Chat.module.css";
export function ConnectedChat(props: ChatProps) {
const chat = useChat(props);
@ -100,7 +101,7 @@ export function Chat({
};
return (
<section className={clsx("chat__container", className, { hidden })}>
<section className={clsx(styles.container, className, { hidden })}>
{rooms.length > 1 ? (
<div className="stack horizontal">
{rooms.map((room) => {
@ -109,30 +110,33 @@ export function Chat({
return (
<Button
key={room.code}
className={clsx("chat__room-button", {
current: currentRoom === room.code,
className={clsx(styles.roomButton, {
[styles.roomButtonCurrent]: currentRoom === room.code,
})}
onPress={() => {
setCurrentRoom(room.code);
resetScroller();
}}
>
<span className="chat__room-button__unseen invisible" />
<span className={clsx(styles.roomButtonUnseen, "invisible")} />
{room.label}
{unseen ? (
<span className="chat__room-button__unseen">{unseen}</span>
<span className={styles.roomButtonUnseen}>{unseen}</span>
) : (
<span className="chat__room-button__unseen invisible" />
<span
className={clsx(styles.roomButtonUnseen, "invisible")}
/>
)}
</Button>
);
})}
</div>
) : null}
<div className="chat__input-container">
<div className={styles.inputContainer}>
<ol
className={clsx(
"chat__messages scrollbar",
styles.messages,
"scrollbar",
messagesContainerClassName,
)}
ref={messagesContainerRef}
@ -164,7 +168,7 @@ export function Chat({
</ol>
{unseenMessagesInTheRoom ? (
<SendouButton
className="chat__unseen-messages"
className={styles.unseenMessages}
onPress={scrollToBottom}
>
{t("common:chat.newMessages")}
@ -178,7 +182,7 @@ export function Chat({
disabled={sendingMessagesDisabled}
maxLength={MESSAGE_MAX_LENGTH}
/>{" "}
<div className="chat__bottom-row">
<div className={styles.bottomRow}>
{readyState === "CONNECTED" || readyState === "CONNECTING" ? (
<div className="text-xxs font-semi-bold text-lighter">
{t(
@ -216,12 +220,12 @@ function Message({
missingUserName?: string;
}) {
return (
<li className="chat__message">
<li className={styles.message}>
{user ? <Avatar user={user} size="xs" /> : null}
<div>
<div className="stack horizontal sm items-center">
<div
className="chat__message__user"
className={styles.messageUser}
style={
user?.chatNameColor
? { "--chat-user-color": user.chatNameColor }
@ -240,8 +244,8 @@ function Message({
) : null}
</div>
<div
className={clsx("chat__message__contents", {
pending: message.pending,
className={clsx(styles.messageContents, {
[styles.messageContentsPending]: message.pending,
})}
>
{message.contents}
@ -259,12 +263,17 @@ function SystemMessage({
text: string;
}) {
return (
<li className="chat__message">
<li className={styles.message}>
<div>
<div className="stack horizontal sm">
<MessageTimestamp timestamp={message.timestamp} />
</div>
<div className="chat__message__contents text-xs text-lighter font-semi-bold">
<div
className={clsx(
styles.messageContents,
"text-xs text-lighter font-semi-bold",
)}
>
{text}
</div>
</div>
@ -277,7 +286,7 @@ function MessageTimestamp({ timestamp }: { timestamp: number }) {
const moreThanDayAgo = sub(new Date(), { days: 1 }) > new Date(timestamp);
return (
<time className="chat__message__time">
<time className={styles.messageTime}>
{moreThanDayAgo
? formatDateTime(new Date(timestamp), {
day: "numeric",

View File

@ -0,0 +1,24 @@
.container {
height: 125px;
width: 125px;
border-radius: 100%;
background-color: var(--color-bg-high);
overflow: hidden;
position: relative;
}
.containerSmall {
height: 41.6667px;
width: 41.6667px;
}
.imgContainer {
position: absolute;
top: var(--winner-top, 5px);
left: var(--winner-left, 25px);
}
.img {
overflow: visible;
max-width: initial;
}

View File

@ -5,6 +5,7 @@ import { Placement } from "~/components/Placement";
import invariant from "~/utils/invariant";
import { winnersImageUrl } from "~/utils/urls";
import playerData from "../top-ten.json";
import styles from "./TopTenPlayer.module.css";
export function TopTenPlayer({
power,
@ -31,12 +32,14 @@ export function TopTenPlayer({
"mt-2": small,
})}
>
<div className={clsx("winner__container", { small })}>
<div
className={clsx(styles.container, { [styles.containerSmall]: small })}
>
<Image
path={winnersImageUrl({ season, placement })}
alt=""
containerClassName="winner__img-container"
className="winner__img"
containerClassName={styles.imgContainer}
className={styles.img}
height={small ? 50 : 150}
containerStyle={
{

View File

@ -0,0 +1,11 @@
.discordIcon {
width: 1rem;
display: inline;
fill: #7289da;
}
.youtubeIcon {
width: 1rem;
display: inline;
fill: #f00;
}

View File

@ -6,6 +6,7 @@ import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { LINKS_PAGE, navIconUrl } from "~/utils/urls";
import links from "../links.json";
import styles from "./links.module.css";
export const handle: SendouRouteHandle = {
breadcrumb: () => ({
@ -46,10 +47,10 @@ export default function LinksPage() {
>
{link.title}
{isDiscord ? (
<DiscordIcon className="discord-icon" />
<DiscordIcon className={styles.discordIcon} />
) : null}
{isYoutube ? (
<YouTubeIcon className="youtube-icon" />
<YouTubeIcon className={styles.youtubeIcon} />
) : null}
</a>
</h2>

View File

@ -0,0 +1,135 @@
.container {
--map-width: 90px;
--map-height: 50px;
}
.slot {
font-size: var(--fonts-xs);
display: grid;
place-items: center;
color: var(--color-text-high);
border-radius: var(--rounded-sm);
font-weight: var(--bold);
width: 24px;
height: 24px;
border: 2px dotted var(--color-bg-higher);
}
.slotIcon {
color: var(--color-success);
width: 16px;
}
.slotPicked {
border-style: solid;
}
.mapImg {
border-radius: var(--rounded-sm);
}
.mapButton {
background-image: var(--map-image-url);
background-size: contain;
height: var(--map-height);
width: var(--map-width);
border: none;
background-color: transparent;
transition:
filter,
opacity 0.2s;
border-radius: var(--rounded);
}
@keyframes wiggle {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(5deg);
}
75% {
transform: rotate(-5deg);
}
100% {
transform: rotate(0deg);
}
}
.mapButtonWiggle {
animation: wiggle 0.25s infinite;
animation-iteration-count: 1;
}
.mapButton:active {
transform: none;
}
.mapButtonGreyedOut {
filter: grayscale(100%) !important;
opacity: 0.4 !important;
}
.mapButtonIcon {
position: absolute;
top: 2px;
fill: var(--color-success);
width: 48px;
cursor: pointer;
}
.mapButtonIconError {
fill: var(--color-error);
}
.mapButtonText {
position: absolute;
top: 14px;
text-transform: uppercase;
font-weight: var(--bold);
cursor: not-allowed;
}
.mapButtonLabel {
font-size: var(--fonts-xxxxs);
color: var(--color-text-high);
font-weight: var(--semi-bold);
}
.mapButtonNumber {
position: absolute;
background-color: var(--color-accent);
border-radius: 100%;
width: 18px;
height: 18px;
display: grid;
place-items: center;
color: var(--color-text-inverse);
font-size: var(--fonts-xxsm);
font-weight: var(--semi-bold);
top: -5px;
left: 0;
}
.mapButtonFrom {
position: absolute;
bottom: -15px;
font-size: var(--fonts-xxs);
font-weight: var(--bold);
}
.divider::before,
.divider::after {
border-bottom: 2px dotted var(--color-bg-higher);
}
.divider {
font-size: var(--fonts-xs);
font-weight: var(--semi-bold);
text-transform: uppercase;
display: flex;
gap: var(--s-2);
}

View File

@ -9,6 +9,7 @@ import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
import { nullFilledArray } from "~/utils/arrays";
import { stageImageUrl } from "~/utils/urls";
import { BANNED_MAPS } from "../banned-maps";
import styles from "./ModeMapPoolPicker.module.css";
export function ModeMapPoolPicker({
mode,
@ -63,7 +64,7 @@ export function ModeMapPoolPicker({
};
return (
<div className="map-pool-picker stack sm">
<div className={clsx(styles.container, "stack sm")}>
<div className="stack sm horizontal justify-center">
{nullFilledArray(amountToPick).map((_, index) => {
return (
@ -75,7 +76,7 @@ export function ModeMapPoolPicker({
);
})}
</div>
<Divider className="map-pool-picker__divider">
<Divider className={styles.divider}>
<ModeImage mode={mode} size={32} />
</Divider>
<div className="stack sm horizontal flex-wrap justify-center mt-1">
@ -113,15 +114,11 @@ export function ModeMapPoolPicker({
function MapSlot({ number, picked }: { number: number; picked: boolean }) {
return (
<div
className={clsx("map-pool-picker__slot", {
"map-pool-picker__slot__picked": picked,
className={clsx(styles.slot, {
[styles.slotPicked]: picked,
})}
>
{picked ? (
<CheckmarkIcon className="map-pool-picker__slot__icon" />
) : (
number
)}
{picked ? <CheckmarkIcon className={styles.slotIcon} /> : number}
</div>
);
}
@ -148,10 +145,9 @@ function MapButton({
return (
<div className="stack items-center relative">
<button
className={clsx("map-pool-picker__map-button", {
"map-pool-picker__map-button__wiggle": wiggle,
"map-pool-picker__map-button__greyed-out":
selected || banned || tiebreaker,
className={clsx(styles.mapButton, {
[styles.mapButtonWiggle]: wiggle,
[styles.mapButtonGreyedOut]: selected || banned || tiebreaker,
})}
style={{ "--map-image-url": `url("${stageImageUrl(stageId)}.png")` }}
onClick={onClick}
@ -160,21 +156,14 @@ function MapButton({
data-testid={testId}
/>
{selected ? (
<CheckmarkIcon
className="map-pool-picker__map-button__icon"
onClick={onClick}
/>
<CheckmarkIcon className={styles.mapButtonIcon} onClick={onClick} />
) : null}
{tiebreaker ? (
<div className="map-pool-picker__map-button__text text-info">
Tiebreak
</div>
<div className={clsx(styles.mapButtonText, "text-info")}>Tiebreak</div>
) : banned ? (
<div className="map-pool-picker__map-button__text text-error">
Banned
</div>
<div className={clsx(styles.mapButtonText, "text-error")}>Banned</div>
) : null}
<div className="map-pool-picker__map-button__label">
<div className={styles.mapButtonLabel}>
{t(`game-misc:STAGE_${stageId}`)}
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -541,3 +541,100 @@
background-clip: content-box;
}
}
.stack {
display: flex;
flex-direction: column;
}
.stack.xxxs {
gap: var(--s-0-5);
}
.stack.xxs {
gap: var(--s-1);
}
.stack.xs {
gap: var(--s-1-5);
}
.stack.sm {
gap: var(--s-2);
}
.stack.sm-column {
column-gap: var(--s-2);
}
.stack.sm-plus {
gap: var(--s-3);
}
.stack.md {
gap: var(--s-4);
}
.stack.md-plus {
gap: var(--s-6);
}
.stack.lg {
gap: var(--s-8);
}
.stack.xs-row {
row-gap: var(--s-1-5);
}
.stack.lg-row {
row-gap: var(--s-8);
}
.stack.xl {
gap: var(--s-12);
}
.stack.xxl {
gap: var(--s-16);
}
.stack.horizontal {
flex-direction: row;
}
.flex-same-size {
flex: 1 1 0px;
}
.small-icon {
width: 1.2rem;
height: 1.2rem;
}
.small-text {
font-size: var(--fonts-xxs) !important;
}
.dotted {
text-decoration-style: dotted;
text-decoration-line: underline;
text-decoration-thickness: 2px;
}
.summary {
border-radius: var(--rounded);
background-color: var(--color-bg);
font-size: var(--fonts-xs);
font-weight: var(--semi-bold);
padding-block: var(--s-1);
padding-inline: var(--s-2);
}
html[dir="rtl"] .fix-rtl {
transform: rotate(180deg);
}
.clickable {
cursor: pointer;
}