TankopediaCharacteristics

This commit is contained in:
tresabhi committed 2026-09-30 18:22:18 -05:00
1 parent 5cae5d1583
commit 74fbd15c5f
9 files changed
+521 -68

No files matched your search

@@ -0,0 +1,138 @@
.characteristics-wrapper {
margin: var(--space-4);
--base-cell-width: 20rem;
--group-span: 7;
container-type: inline-size;
.characteristics {
--N: round(down, 100cqw / var(--base-cell-width));
--cell-width: calc(100cqw / var(--N));
--cell-height: calc(var(--cell-width) / var(--group-span));
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(var(--base-cell-width), 1fr)
);
grid-auto-rows: var(--cell-height);
grid-auto-flow: dense;
box-sizing: border-box;
.group {
--gradient-size: min(100%, var(--space-8));
grid-column: span 1;
display: grid;
grid-template-columns: inherit;
grid-auto-rows: inherit;
.title {
grid-row: span 1;
display: flex;
align-items: center;
margin: 0 calc(var(--space-4));
.center {
flex: 1;
padding: var(--space-3);
.text {
.align {
display: flex;
align-items: center;
gap: var(--space-3);
img {
width: 1em;
height: 1em;
}
}
}
}
}
.content {
margin: 0 var(--space-4);
gap: inherit;
height: 100%;
display: grid;
grid-template-columns: inherit;
grid-auto-rows: inherit;
.item {
padding: 0 var(--space-3);
grid-row: span 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: var(--space-2);
.value {
display: flex;
justify-content: space-between;
.units {
display: flex;
align-items: end;
gap: var(--space-2);
}
}
.ranking {
display: flex;
align-items: center;
gap: var(--space-3);
.bar-wrapper {
flex: 1;
height: var(--space-1);
background-color: var(--gray-3);
border-radius: var(--radius-max);
overflow: hidden;
.bar {
height: 100%;
}
}
}
}
.toy {
grid-row: span var(--group-span);
padding: var(--space-4) 0;
.sandbox {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--gray-2);
outline: var(--gray-5) solid var(--space-1);
border-radius: var(--space-3);
}
}
.dead {
grid-row: span 1;
}
}
.dead {
grid-row: span 1;
}
}
}
}
@@ -0,0 +1,183 @@
import { characteristicsGroups } from "../../config/characteristicsGroups";
import { useCharacteristicRenderer } from "../../hooks/useCharacteristicRenderer";
import { useStrings } from "../../hooks/useStrings";
import type {
CharacteristicRenderConfig,
CharacteristicReturnType,
CharacteristicsGroup,
ComputedCharacteristics,
} from "../../types/characteristics";
import { Heading } from "../Heading";
import { Text } from "../Text";
import styles from "./index.module.css";
interface TankopediaCharacteristicsProps {
characteristics: ComputedCharacteristics;
}
export function TankopediaCharacteristics({
characteristics,
}: TankopediaCharacteristicsProps) {
return (
<div className={styles["characteristics-wrapper"]}>
<div className={styles.characteristics}>
{characteristicsGroups.map((group) => {
const items = group.order.filter(
(item) => "name" in item && characteristics[item.name] !== null,
).length;
const shouldRender = items > 0;
if (!shouldRender) return null;
return (
<Group
key={group.name}
group={group}
characteristics={characteristics}
/>
);
})}
</div>
</div>
);
}
interface GroupProps {
group: CharacteristicsGroup;
characteristics: ComputedCharacteristics;
}
function Group({ group, characteristics }: GroupProps) {
const strings = useStrings();
const toys = group.order.filter((item) => "toy" in item).length;
const items = group.order.filter(
(item) => "name" in item && characteristics[item.name] !== null,
).length;
const groupSpan = `calc(${toys} * var(--group-span) + ${items} + 2)`;
const contentSpan = `calc(${toys} * var(--group-span) + ${items})`;
return (
<div
className={styles.group}
style={{
gridRow: `span ${groupSpan}`,
gridTemplateRows: `repeat(${groupSpan}, 1fr)`,
}}
>
<div className={styles.title}>
<div className={styles.center}>
<Heading size="3" className={styles.text}>
<div className={styles.align}>
{
(
strings.tanks.characteristics.titles as Record<string, string>
)[group.name]
}
<img src={`/media/groups/${group.name}.png`} />
</div>
</Heading>
</div>
</div>
<div
className={styles.content}
style={{
gridRow: `span ${contentSpan}`,
}}
>
{group.order.map((item) => (
<>
{"toy" in item && <Toy {...item} />}
{"name" in item && characteristics[item.name] !== null && (
<Item config={item} value={characteristics[item.name]!} />
)}
</>
))}
<div className={styles.dead} />
</div>
</div>
);
}
interface ItemProps {
config: CharacteristicRenderConfig;
value: CharacteristicReturnType;
}
function Item({ config, value }: ItemProps) {
const strings = useStrings();
const renderCharacteristic = useCharacteristicRenderer();
const hasUnits = config.units !== undefined;
return (
<span className={styles.item}>
<div className={styles.value}>
<Text lowContrast>
{
(strings.tanks.characteristics.items as Record<string, string>)[
config.name
]
}
</Text>
<Text lowContrast={hasUnits} size={hasUnits ? "minor" : undefined}>
<div className={styles.units}>
{renderCharacteristic(value, config)}
</div>
</Text>
</div>
<div className={styles.ranking}>
<Bar value={0.5} />
<Text lowContrast size="minor" className={styles.number}>
1 / 132
</Text>
</div>
</span>
);
}
interface BarProps {
value: number;
}
const barColors = ["red", "orange", "amber", "jade"];
function Bar({ value }: BarProps) {
const index = (barColors.length - 1) * value;
const iA = Math.floor(index);
const iB = Math.ceil(index);
const A = barColors[iA];
const B = barColors[iB];
const x = index % 1;
const p = (1 - x) * 100;
const v = 7;
return (
<div className={styles["bar-wrapper"]}>
<div
className={styles.bar}
style={{
backgroundColor: `color-mix(in srgb, var(--${A}-${v}) ${p}%, var(--${B}-${v}))`,
width: `${value * 100}%`,
}}
/>
</div>
);
}
interface ToyProps {
toy: string;
}
function Toy({ toy }: ToyProps) {
return (
<div className={styles.toy}>
<div className={styles.sandbox}>
<Text lowContrast>{toy} toy</Text>
</div>
</div>
);
}