mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
migrate 3
This commit is contained in:
1 parent
de8b3959b3
commit
5285d081e4
29 files changed
+740
-83
No files matched your search
@@ -424,7 +424,7 @@
|
||||
"tier": "Tier {tier}"
|
||||
},
|
||||
|
||||
"tankopedia": {
|
||||
"tanks": {
|
||||
"search": "Search a stat...",
|
||||
|
||||
"name": "Tankopedia",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"markdown-to-jsx": "^7.7.13",
|
||||
"pwa-install-handler": "^2.6.3",
|
||||
"quicklime": "^1.2.0",
|
||||
"radix-ui": "^1.6.7",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-promise-suspense": "^0.3.4",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
|
||||
border-radius: var(--max-size);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
|
||||
border: none;
|
||||
|
||||
font-family: var(--font-family-regular);
|
||||
font-size: 0.75rem;
|
||||
color: var(--gray-1);
|
||||
font-weight: var(--weight-medium);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&[data-size="minor"] {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
&[data-array="true"] {
|
||||
&:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-parent-array="true"] {
|
||||
*:first-child > & {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
*:last-child > & {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
svg,
|
||||
img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
&[data-light-text="true"] {
|
||||
color: var(--white-a12);
|
||||
}
|
||||
|
||||
&[data-variant="solid"] {
|
||||
background-color: var(--accent-8);
|
||||
}
|
||||
|
||||
&[data-variant="surface"],
|
||||
&[data-variant="soft"] {
|
||||
background-color: var(--accent-3);
|
||||
color: var(--gray-12);
|
||||
}
|
||||
|
||||
&[data-variant="inverted"] {
|
||||
background-color: var(--accent-12);
|
||||
color: var(--accent-1);
|
||||
}
|
||||
|
||||
&[data-variant="ghost"],
|
||||
&[data-variant="outline"] {
|
||||
background: none;
|
||||
color: var(--accent-12);
|
||||
}
|
||||
|
||||
&[data-variant="ghost"] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&[data-variant="surface"],
|
||||
&[data-variant="outline"] {
|
||||
outline: var(--space-1) solid var(--accent-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { classNames } from "../../ui/classNames";
|
||||
import type { Color } from "../../ui/color";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
export interface ButtonProps extends ComponentProps<"button"> {
|
||||
color?: Color;
|
||||
radius?: `${1 | 2 | 3}` | "max";
|
||||
variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline";
|
||||
size?: "regular" | "minor";
|
||||
array?: boolean;
|
||||
parentArray?: boolean;
|
||||
}
|
||||
|
||||
export const LIGHT_TEXT_COLORS = new Set<Color>([
|
||||
"gold",
|
||||
"bronze",
|
||||
"teal",
|
||||
"grass",
|
||||
"amber",
|
||||
"plum",
|
||||
"pink",
|
||||
"brown",
|
||||
"crimson",
|
||||
"jade",
|
||||
"blue",
|
||||
"tomato",
|
||||
]);
|
||||
|
||||
export function Button({
|
||||
className,
|
||||
color,
|
||||
variant = "solid",
|
||||
style,
|
||||
radius = "max",
|
||||
size = "regular",
|
||||
array = false,
|
||||
parentArray = false,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={classNames("composable-radius", styles.button, className)}
|
||||
data-accent={color}
|
||||
data-variant={variant}
|
||||
data-light-text={variant === "solid"}
|
||||
data-radius={radius}
|
||||
data-size={size}
|
||||
data-array={array}
|
||||
data-parent-array={parentArray}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,10 @@
|
||||
import type { ReactNode } from "react";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Callout({ children }: Props) {
|
||||
return <div className={styles.callout}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
Menubar,
|
||||
MenubarCheckboxItem,
|
||||
MenubarContent,
|
||||
MenubarLabel,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarSub,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarTrigger,
|
||||
type MenubarCheckboxItemProps,
|
||||
type MenubarContentProps,
|
||||
type MenubarLabelProps,
|
||||
type MenubarProps,
|
||||
type MenubarRadioGroupProps,
|
||||
type MenubarRadioItemProps,
|
||||
type MenubarSubContentProps,
|
||||
type MenubarSubProps,
|
||||
type MenubarSubTriggerProps,
|
||||
type MenubarTriggerProps,
|
||||
} from "radix-ui/menubar";
|
||||
|
||||
function Root(props: MenubarProps) {
|
||||
return <Menubar {...props} />;
|
||||
}
|
||||
|
||||
function Trigger(props: MenubarTriggerProps) {
|
||||
return <MenubarTrigger {...props} />;
|
||||
}
|
||||
|
||||
function Content(props: MenubarContentProps) {
|
||||
return <MenubarContent {...props} />;
|
||||
}
|
||||
|
||||
function Sub(props: MenubarSubProps) {
|
||||
return <MenubarSub {...props} />;
|
||||
}
|
||||
|
||||
function SubTrigger(props: MenubarSubTriggerProps) {
|
||||
return <MenubarSubTrigger {...props} />;
|
||||
}
|
||||
|
||||
function SubContent(props: MenubarSubContentProps) {
|
||||
return <MenubarSubContent {...props} />;
|
||||
}
|
||||
|
||||
function Label(props: MenubarLabelProps) {
|
||||
return <MenubarLabel {...props} />;
|
||||
}
|
||||
|
||||
function CheckboxItem(props: MenubarCheckboxItemProps) {
|
||||
return <MenubarCheckboxItem {...props} />;
|
||||
}
|
||||
|
||||
function RadioGroup(props: MenubarRadioGroupProps) {
|
||||
return <MenubarRadioGroup {...props} />;
|
||||
}
|
||||
|
||||
function RadioItem(props: MenubarRadioItemProps) {
|
||||
return <MenubarRadioItem {...props} />;
|
||||
}
|
||||
|
||||
export const DropdownMenu = {
|
||||
Root,
|
||||
Trigger,
|
||||
Content,
|
||||
Sub,
|
||||
SubTrigger,
|
||||
SubContent,
|
||||
Label,
|
||||
CheckboxItem,
|
||||
RadioGroup,
|
||||
RadioItem,
|
||||
};
|
||||
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
||||
export function Spinner() {
|
||||
return null;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { alias, TankType, type TankDefinition } from "@blitzkit/core";
|
||||
import { Flex, Text, type TextProps } from "@radix-ui/themes";
|
||||
import { uniq } from "lodash-es";
|
||||
import { forwardRef, type ReactNode } from "react";
|
||||
import { api } from "../../core/blitzkit/api";
|
||||
|
||||
@@ -15,16 +15,6 @@ import {
|
||||
ResetIcon,
|
||||
TrashIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
Flex,
|
||||
IconButton,
|
||||
Text,
|
||||
Tooltip,
|
||||
type FlexProps,
|
||||
} from "@radix-ui/themes";
|
||||
import { times } from "lodash-es";
|
||||
import { Fragment, type ComponentProps, type ReactNode } from "react";
|
||||
import { api } from "../../../core/blitzkit/api";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Flex, Link, Text } from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { type TankDefinition } from "@blitzkit/core";
|
||||
import { CaretRightIcon } from "@radix-ui/react-icons";
|
||||
import { Button, TextField } from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { LinkI18n } from "../../LinkI18n";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Flex, Separator, Text } from "@radix-ui/themes";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { useMemo } from "react";
|
||||
import { api } from "../../../core/blitzkit/api";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Flex, Skeleton } from "@radix-ui/themes";
|
||||
import { memo, useRef } from "react";
|
||||
import { useIntersection } from "../../../hooks/useIntersection";
|
||||
|
||||
@@ -21,5 +20,5 @@ export const SkeletonTankCard = memo(
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
() => true
|
||||
() => true,
|
||||
);
|
||||
@@ -1,11 +1,12 @@
|
||||
import { CaretDownIcon } from '@radix-ui/react-icons';
|
||||
import { Button, DropdownMenu } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { CaretDownIcon } from "@radix-ui/react-icons";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import type {
|
||||
TankopediaSortBy,
|
||||
TankopediaSortDirection,
|
||||
} from '../../../stores/tankopediaPersistent';
|
||||
import { TankSort } from '../../../stores/tankopediaSort';
|
||||
} from "../../../stores/tankopediaPersistent";
|
||||
import { TankSort } from "../../../stores/tankopediaSort";
|
||||
import { Button } from "../../Button";
|
||||
import { DropdownMenu } from "../../DropdownMenu";
|
||||
|
||||
interface ItemProps {
|
||||
by: TankopediaSortBy;
|
||||
@@ -38,7 +39,7 @@ export function Sort() {
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root modal={false}>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="surface" color="gray">
|
||||
{strings.website.common.tank_search.sort_dropdown.label}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Grid, type GridProps } from "@radix-ui/themes";
|
||||
|
||||
export function TankCardWrapper(props: GridProps) {
|
||||
return (
|
||||
<Grid
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: var(--space-2);
|
||||
flex-grow: 1;
|
||||
|
||||
.tanks {
|
||||
/* mt="2" gap="1" align="center" justify="center" direction="column" */
|
||||
|
||||
.count {
|
||||
/* gap="2" */
|
||||
}
|
||||
}
|
||||
|
||||
.test-warning {
|
||||
/* justify="center" mt="4" */
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,9 @@ import {
|
||||
type TankDefinition,
|
||||
} from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import { Callout, Flex, Link, Text, type FlexProps } from "@radix-ui/themes";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { times, uniq } from "lodash-es";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import { memo, useEffect, useMemo, useState, type ComponentProps } from "react";
|
||||
import usePromise from "react-promise-suspense";
|
||||
import { awaitableTankNames } from "../../core/awaitables/tankNames";
|
||||
import { api } from "../../core/blitzkit/api";
|
||||
@@ -23,18 +22,22 @@ import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
||||
import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants";
|
||||
import { TankSort } from "../../stores/tankopediaSort";
|
||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||
import { Callout } from "../Callout";
|
||||
import { ExperimentIcon } from "../ExperimentIcon";
|
||||
import { Link } from "../Link";
|
||||
import { Text } from "../Text";
|
||||
import { TankSearchCard } from "./components/Card";
|
||||
import { FilterControl } from "./components/FilterControl";
|
||||
import { NoResults } from "./components/NoResults";
|
||||
import { RecentlyViewed } from "./components/RecentlyViewed";
|
||||
import { SearchBar } from "./components/SearchBar";
|
||||
import { TankSearchBar } from "../TankSearchBar";
|
||||
import { SkeletonTankCard } from "./components/SkeletonTankCard";
|
||||
import { TankCardWrapper } from "./components/TankCardWrapper";
|
||||
import { MAX_RECENTLY_VIEWED } from "./constants";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
export type TankSearchProps = MaybeSkeletonComponentProps &
|
||||
Omit<FlexProps, "onSelect"> & {
|
||||
ComponentProps<"div"> & {
|
||||
compact?: boolean;
|
||||
onSelect?: (tank: TankDefinition) => void;
|
||||
onSelectAll?: (tanks: TankDefinition[]) => void;
|
||||
@@ -427,8 +430,8 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
}, [tankFilters, tankopediaSort]);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" flexGrow="1" {...props}>
|
||||
<SearchBar
|
||||
<div className={styles.container} {...props}>
|
||||
<TankSearchBar
|
||||
skeleton={skeleton}
|
||||
topResult={tanks?.[0]}
|
||||
onSelect={onSelect}
|
||||
@@ -438,9 +441,9 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
|
||||
{!skeleton && !compact && <RecentlyViewed />}
|
||||
|
||||
<Flex mt="2" gap="1" align="center" justify="center" direction="column">
|
||||
<Flex gap="2">
|
||||
<Text color="gray">
|
||||
<div className={styles.tanks}>
|
||||
<div className={styles.count}>
|
||||
<Text lowContrast>
|
||||
{sorted.length === 1
|
||||
? strings.website.common.tank_search.count_singular
|
||||
: literals(strings.website.common.tank_search.count_plural, {
|
||||
@@ -468,7 +471,7 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
{strings.website.common.tank_search.select_all}
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
{tankopediaSort.by !== "meta.none" && (
|
||||
<Text color="gray">
|
||||
@@ -483,19 +486,15 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
, {tankopediaSort.direction}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
{tankFilters.showTesting && !tankFilters.showNonTesting && (
|
||||
<Flex justify="center" mt="4">
|
||||
<Callout.Root color="amber">
|
||||
<Callout.Icon>
|
||||
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
|
||||
</Callout.Icon>
|
||||
<Callout.Text>
|
||||
{strings.website.common.warnings.test}
|
||||
</Callout.Text>
|
||||
</Callout.Root>
|
||||
</Flex>
|
||||
<div className={styles["test-warning"]}>
|
||||
<Callout color="amber">
|
||||
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
|
||||
{strings.website.common.warnings.test}
|
||||
</Callout>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!skeleton && !tankFilters.searching && (
|
||||
@@ -544,7 +543,7 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
)}
|
||||
</TankCardWrapper>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
.search-bar {
|
||||
/* justify="center" */
|
||||
|
||||
.wrapper {
|
||||
/* gap="2" flexGrow="1" */
|
||||
}
|
||||
}
|
||||
+21
-21
@@ -1,20 +1,25 @@
|
||||
import type { TankDefinition } from "@blitzkit/core";
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { Flex, Spinner, TextField } from "@radix-ui/themes";
|
||||
import { debounce } from "lodash-es";
|
||||
import { type KeyboardEventHandler, useCallback, useRef } from "react";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
import { QuickLink } from "./QuickLink";
|
||||
import { Sort } from "./Sort";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||
import { Spinner } from "../Spinner";
|
||||
import { Sort } from "../TankSearch/components/Sort";
|
||||
import { TextField } from "../TextField";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
type SearchBarProps = MaybeSkeletonComponentProps & {
|
||||
topResult?: TankDefinition;
|
||||
onSelect?: (tank: TankDefinition) => void;
|
||||
};
|
||||
|
||||
export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
export function TankSearchBar({
|
||||
topResult,
|
||||
skeleton,
|
||||
onSelect,
|
||||
}: SearchBarProps) {
|
||||
const { strings } = useLocale();
|
||||
const search = TankFilters.use((state) => state.search);
|
||||
const searching = TankFilters.use((state) => state.searching);
|
||||
@@ -33,7 +38,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
draft.search = sanitized.length === 0 ? null : sanitized;
|
||||
});
|
||||
}, 500),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
const handleChange = useCallback(() => {
|
||||
if (!searching) {
|
||||
@@ -56,14 +61,13 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
window.location.href = `/tanks/${topResult.slug}`;
|
||||
}
|
||||
},
|
||||
[topResult]
|
||||
[topResult],
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex justify="center">
|
||||
<Flex gap="2" flexGrow="1">
|
||||
<TextField.Root
|
||||
variant="classic"
|
||||
<div className={styles["search-bar"]}>
|
||||
<div className={styles.wrapper}>
|
||||
<TextField
|
||||
disabled={skeleton}
|
||||
defaultValue={search ?? undefined}
|
||||
style={{ flex: 1 }}
|
||||
@@ -72,15 +76,11 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<TextField.Slot>
|
||||
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
||||
</TextField.Slot>
|
||||
|
||||
<QuickLink topResult={topResult} />
|
||||
</TextField.Root>
|
||||
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
||||
</TextField>
|
||||
|
||||
<Sort />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
.text-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
gap: var(--space-3);
|
||||
|
||||
cursor: text;
|
||||
|
||||
font: inherit;
|
||||
color: var(--gray-11);
|
||||
|
||||
&[data-variant="bubble"] {
|
||||
border-radius: var(--max-size);
|
||||
|
||||
background-color: var(--gray-1);
|
||||
outline: var(--outline-interactive);
|
||||
|
||||
input {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant="form"] {
|
||||
border-bottom: var(--space-1) solid var(--gray-6);
|
||||
|
||||
input {
|
||||
padding: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-has-icon="true"] {
|
||||
padding-left: var(--space-3);
|
||||
|
||||
input {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
outline: var(--outline-focus);
|
||||
box-shadow: var(--shadow-focus);
|
||||
}
|
||||
|
||||
&[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
background-color: var(--gray-3);
|
||||
|
||||
color: var(--gray-9);
|
||||
|
||||
input::placeholder {
|
||||
color: var(--gray-6);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: inherit;
|
||||
|
||||
font: inherit;
|
||||
|
||||
color: var(--gray-12);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--gray-9);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-align="center"] {
|
||||
text-align: center;
|
||||
}
|
||||
&[data-align="right"] {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
type ChangeEvent,
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { classNames } from "../../ui/classNames";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
export interface TextFieldProps extends ComponentProps<"input"> {
|
||||
children?: ReactNode;
|
||||
align?: "left" | "center" | "right";
|
||||
autoWidth?: boolean;
|
||||
variant?: "bubble" | "form";
|
||||
}
|
||||
|
||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
autoWidth,
|
||||
placeholder,
|
||||
align,
|
||||
disabled,
|
||||
value,
|
||||
onChange,
|
||||
variant = "bubble",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (autoWidth && input.current) {
|
||||
input.current.style.width = `${event.target.value.length || event.target.placeholder.length}ch`;
|
||||
}
|
||||
|
||||
onChange?.(event);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => input.current!, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={classNames(styles["text-field"], className)}
|
||||
data-has-icon={children !== undefined}
|
||||
data-disabled={disabled}
|
||||
data-variant={variant}
|
||||
>
|
||||
{children}
|
||||
|
||||
<input
|
||||
data-align={align === "left" ? undefined : align}
|
||||
disabled={disabled}
|
||||
ref={input}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
style={{
|
||||
width: `${value?.toString().length || placeholder?.length || 0}ch`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
import type { Tool } from "../constants/tools";
|
||||
import { getStrings } from "../core/i18n/getStrings";
|
||||
import { Heading } from "./Heading";
|
||||
|
||||
interface Props {
|
||||
locale: string;
|
||||
tool: Tool;
|
||||
}
|
||||
|
||||
const { tool, locale } = Astro.props;
|
||||
const strings = getStrings(locale);
|
||||
const toolStrings =
|
||||
strings.website.tools[tool.id as keyof typeof strings.website.tools];
|
||||
---
|
||||
|
||||
<style>
|
||||
.tool-banner {
|
||||
background-size: cover;
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-5) var(--space-4);
|
||||
box-sizing: border-box;
|
||||
|
||||
backdrop-filter: var(--blur-tint);
|
||||
|
||||
:root.light & {
|
||||
backdrop-filter: var(--blur-tint) invert(1) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
@media (width > 40rem) {
|
||||
padding: var(--space-6) var(--space-4);
|
||||
}
|
||||
|
||||
.centered {
|
||||
flex: 1;
|
||||
max-width: var(--page-width);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
:root.light & {
|
||||
filter: invert(1) brightness(0);
|
||||
}
|
||||
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 25%,
|
||||
var(--black-a1),
|
||||
transparent
|
||||
);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
class="tool-banner"
|
||||
style={{
|
||||
backgroundImage: `url(/media/tools/banners/${tool.id}.webp)`,
|
||||
backgroundPosition: tool.banner_background_position ?? "top",
|
||||
}}
|
||||
>
|
||||
<div class="content">
|
||||
<div
|
||||
class="pattern"
|
||||
style={{ backgroundImage: `url(/media/patterns/${tool.id}.svg)` }}
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="centered">
|
||||
<Heading weight="bold" size="2">
|
||||
{toolStrings.name}
|
||||
</Heading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,58 +2,75 @@ export interface Tool {
|
||||
id: string;
|
||||
|
||||
path?: string;
|
||||
strings?: string;
|
||||
|
||||
href?: string;
|
||||
|
||||
banner_background_position: "top" | "center" | "bottom";
|
||||
}
|
||||
|
||||
export const tools: Record<string, Tool> = {
|
||||
tanks: {
|
||||
id: "tanks",
|
||||
strings: "tankopedia",
|
||||
banner_background_position: "top",
|
||||
},
|
||||
players: {
|
||||
id: "players",
|
||||
strings: "players",
|
||||
banner_background_position: "top",
|
||||
},
|
||||
compare: {
|
||||
id: "compare",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
performance: {
|
||||
id: "performance",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
playlist: {
|
||||
id: "playlist",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
mixer: {
|
||||
id: "mixer",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
guesser: {
|
||||
id: "guesser",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
avatars: {
|
||||
id: "avatars",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
backgrounds: {
|
||||
id: "backgrounds",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
session: {
|
||||
id: "session",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
tier_list: {
|
||||
id: "tier_list",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
embed: {
|
||||
id: "embed",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
more: {
|
||||
id: "more",
|
||||
href: "https://discord.gg/nDt7AjGJQH",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
api: {
|
||||
id: "api",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
changelogs: {
|
||||
id: "changelogs",
|
||||
banner_background_position: "bottom",
|
||||
},
|
||||
settings: {
|
||||
id: "changelogs",
|
||||
banner_background_position: "center",
|
||||
},
|
||||
};
|
||||
@@ -1,10 +1,8 @@
|
||||
import { createDefaultSkills } from "@blitzkit/core";
|
||||
import { Flex, Heading, Text } from "@radix-ui/themes";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { CompareTable } from "../../../../components/Compare/CompareTable";
|
||||
import { Controls } from "../../../../components/Compare/Controls";
|
||||
import { DamageWarning } from "../../../../components/DamageWarning";
|
||||
import { PageWrapper } from "../../../../components/PageWrapper";
|
||||
import { api } from "../../../../core/blitzkit/api";
|
||||
import { tankCharacteristics } from "../../../../core/blitzkit/tankCharacteristics";
|
||||
import { tankToCompareMember } from "../../../../core/blitzkit/tankToCompareMember";
|
||||
@@ -118,7 +116,7 @@ function Content() {
|
||||
}, [members]);
|
||||
|
||||
return (
|
||||
<PageWrapper color="crimson" maxWidth="100%" px="0" pb="0">
|
||||
<>
|
||||
<Flex justify="center" gap="4" align="center" direction="column">
|
||||
<DamageWarning />
|
||||
<Controls
|
||||
@@ -148,6 +146,6 @@ function Content() {
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PageWrapper } from "../../../../components/PageWrapper";
|
||||
import { TankSearch } from "../../../../components/TankSearch";
|
||||
import {
|
||||
LocaleProvider,
|
||||
@@ -12,9 +11,7 @@ export function Page({
|
||||
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<PageWrapper color="purple" maxWidth="80rem">
|
||||
<TankSearch skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
<TankSearch skeleton={skeleton} />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import ToolBanner from "../../../../components/ToolBanner.astro";
|
||||
import { tools } from "../../../../constants/tools";
|
||||
import ToolLayout from "../../../../layouts/ToolLayout.astro";
|
||||
import BaseLayout from "../../../../layouts/BaseLayout.astro";
|
||||
import { Page } from "./_index";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
@@ -8,8 +9,10 @@ export { getStaticPaths } from "../index.astro";
|
||||
const { locale } = Astro.props;
|
||||
---
|
||||
|
||||
<ToolLayout {locale} tool={tools.tanks}>
|
||||
<BaseLayout {locale} accent="purple">
|
||||
<ToolBanner {locale} tool={tools.tanks} slot="pre-app" />
|
||||
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton slot="fallback" />
|
||||
</Page>
|
||||
</ToolLayout>
|
||||
</BaseLayout>
|
||||
Reference in new issue
Block a user