mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-05 20:56:13 -05:00
Refactor stuff from common.css to CSS Modules
This commit is contained in:
parent
46814e86cc
commit
3977dccd64
56
app/components/Alert.module.css
Normal file
56
app/components/Alert.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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} />{" "}
|
||||
|
|
|
|||
4
app/components/Avatar.module.css
Normal file
4
app/components/Avatar.module.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.avatar {
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-bg-higher);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
34
app/components/Chart.module.css
Normal file
34
app/components/Chart.module.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
80
app/components/CustomizedColorsInput.module.css
Normal file
80
app/components/CustomizedColorsInput.module.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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 ? (
|
||||
|
|
|
|||
28
app/components/Divider.module.css
Normal file
28
app/components/Divider.module.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
7
app/components/FormErrors.module.css
Normal file
7
app/components/FormErrors.module.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.container {
|
||||
font-size: var(--fonts-sm);
|
||||
|
||||
& > h4 {
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) => (
|
||||
|
|
|
|||
13
app/components/FormMessage.module.css
Normal file
13
app/components/FormMessage.module.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
8
app/components/Image.module.css
Normal file
8
app/components/Image.module.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.tierContainer {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.tierImg {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
24
app/components/InfoPopover.module.css
Normal file
24
app/components/InfoPopover.module.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
60
app/components/Input.module.css
Normal file
60
app/components/Input.module.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
24
app/components/Label.module.css
Normal file
24
app/components/Label.module.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
44
app/components/Pagination.module.css
Normal file
44
app/components/Pagination.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
8
app/components/RequiredHiddenInput.module.css
Normal file
8
app/components/RequiredHiddenInput.module.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
12
app/components/Section.module.css
Normal file
12
app/components/Section.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
63
app/components/SubNav.module.css
Normal file
63
app/components/SubNav.module.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
38
app/components/Table.module.css
Normal file
38
app/components/Table.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
14
app/components/TimePopover.module.css
Normal file
14
app/components/TimePopover.module.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
18
app/components/YouTubeEmbed.module.css
Normal file
18
app/components/YouTubeEmbed.module.css
Normal 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%;
|
||||
}
|
||||
|
|
@ -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}`}
|
||||
|
|
|
|||
81
app/features/art/components/ArtGrid.module.css
Normal file
81
app/features/art/components/ArtGrid.module.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
12
app/features/articles/routes/a.module.css
Normal file
12
app/features/articles/routes/a.module.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
49
app/features/calendar/components/Tags.module.css
Normal file
49
app/features/calendar/components/Tags.module.css
Normal 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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
110
app/features/chat/components/Chat.module.css
Normal file
110
app/features/chat/components/Chat.module.css
Normal 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%);
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
24
app/features/leaderboards/components/TopTenPlayer.module.css
Normal file
24
app/features/leaderboards/components/TopTenPlayer.module.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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={
|
||||
{
|
||||
|
|
|
|||
11
app/features/links/routes/links.module.css
Normal file
11
app/features/links/routes/links.module.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.discordIcon {
|
||||
width: 1rem;
|
||||
display: inline;
|
||||
fill: #7289da;
|
||||
}
|
||||
|
||||
.youtubeIcon {
|
||||
width: 1rem;
|
||||
display: inline;
|
||||
fill: #f00;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user