Compare commits

..
9 Commits
Author SHA1 Message Date
TrèsAbhi 4e586253c0 feat: smaller heaings 2026-03-19 18:48:50 -05:00
TrèsAbhi 08980158e4 fix: tool banner alignment 2026-03-19 18:47:58 -05:00
TrèsAbhi 1e917c7b89 feat: big 2026-03-19 18:45:30 -05:00
TrèsAbhi 8d3678ddcb feat: macro names 2026-03-19 18:44:00 -05:00
TrèsAbhi 6a58c5ea1d feat: form fields 2026-03-19 18:31:00 -05:00
TrèsAbhi b9bbf22f2c feat: more decoration 2026-03-19 18:26:08 -05:00
TrèsAbhi c99ed0cb53 feat: types 2026-03-19 18:17:36 -05:00
TrèsAbhi cef18d4f64 feat: see macro 2026-03-19 17:17:23 -05:00
TrèsAbhi 824169fc25 feat: zooweemama 2026-03-19 13:26:34 -05:00
24 changed files with 449 additions and 62 deletions

No files matched your search

+1
View File
@@ -249,6 +249,7 @@
"winrate",
"wireframe",
"Woah",
"wooorm",
"WOTB",
"wotblitz",
"wotbreplay",
+8
View File
@@ -210,12 +210,14 @@
"@types/react-dom": "^19.2.2",
"@types/three": "^0.180.0",
"@vite-pwa/astro": "^1.1.0",
"@wooorm/starry-night": "^3.9.0",
"astro": "^5.14.4",
"astro-favicons": "^3.1.5",
"autoprefixer": "^10.4.23",
"cssnano": "^7.1.2",
"fuzzysort": "^3.1.0",
"googleapis": "^171.1.0",
"hast-util-to-jsx-runtime": "^2.3.6",
"immer": "^10.1.3",
"lodash-es": "^4.17.21",
"postcss": "^8.5.6",
@@ -1262,6 +1264,8 @@
"@webgpu/types": ["@webgpu/[email protected]", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
"@wooorm/starry-night": ["@wooorm/[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0", "import-meta-resolve": "^4.0.0", "vscode-oniguruma": "^2.0.0", "vscode-textmate": "^9.0.0" } }, "sha512-LXVGKfYhTuFhoRuPAHz2XolS/J45L4lI/lCSIBugDpklXYDUPrJyA8tk17u7G32fcFaGODJXH/YDwQDfUXdPRw=="],
"abort-controller": ["[email protected]", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -2932,6 +2936,10 @@
"vscode-nls": ["[email protected]", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="],
"vscode-oniguruma": ["[email protected]", "", {}, "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ=="],
"vscode-textmate": ["[email protected]", "", {}, "sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q=="],
"vscode-uri": ["[email protected]", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"web-namespaces": ["[email protected]", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
+37 -8
View File
@@ -1,16 +1,45 @@
export function literals(
string: string,
literals?: Record<string, string | number | undefined>
literals?: Record<string, string | number | undefined>,
) {
for (const key in literals) {
const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`);
if (import.meta.env.DEV && mutated === string) {
console.warn(`Could not find literal {${key}} in string "${string}"`);
}
string = mutated;
string = string.replaceAll(`{${key}}`, `${literals[key]}`);
}
return string;
}
export function literalsArray<Type, Literal = Type | string>(
string: string,
literals?: Record<string, Literal>,
) {
if (!literals) return [string];
let array: (string | Literal)[] = [string];
for (const key in literals) {
const value = literals[key];
const next: (string | Literal)[] = [];
for (const chunk of array) {
if (typeof chunk !== "string") {
next.push(chunk);
continue;
}
const parts = chunk.split(`{${key}}`);
for (let index = 0; index < parts.length; index++) {
if (parts[index]) next.push(parts[index]);
if (index < parts.length - 1) {
next.push(value);
}
}
}
array = next;
}
return array;
}
+2 -1
View File
@@ -122,6 +122,7 @@
"pb": "Protocol Buffer methods",
"media": "Media methods",
"no_docs": "This method has no documentation.",
"test": "Test"
"test": "Test",
"view_full": "View full types"
}
}
+2
View File
@@ -29,12 +29,14 @@
"@types/react-dom": "^19.2.2",
"@types/three": "^0.180.0",
"@vite-pwa/astro": "^1.1.0",
"@wooorm/starry-night": "^3.9.0",
"astro": "^5.14.4",
"astro-favicons": "^3.1.5",
"autoprefixer": "^10.4.23",
"cssnano": "^7.1.2",
"fuzzysort": "^3.1.0",
"googleapis": "^171.1.0",
"hast-util-to-jsx-runtime": "^2.3.6",
"immer": "^10.1.3",
"lodash-es": "^4.17.21",
"postcss": "^8.5.6",
@@ -0,0 +1,81 @@
---
import ApiMacroSee from "./ApiMacroSee.astro";
import ApiMacroType from "./ApiMacroType.astro";
import { MarkdownCustom } from "./MarkdownCustom";
interface Props {
body: string;
locale: string;
}
const macroPattern = /@(\w+) (.+)/;
const macros: Record<string, string[]> = {};
const macroComponents = {
see: ApiMacroSee,
type: ApiMacroType,
};
const { body: bodyOriginal, locale } = Astro.props;
let body = "";
for (const line of bodyOriginal.split("\n")) {
const match = line.match(macroPattern);
if (match === null) {
body += `${line}\n`;
continue;
}
const macro = match[1];
const value = match[2];
if (macro in macros) {
macros[macro].push(value);
} else {
macros[macro] = [value];
}
}
---
<style>
.api-description {
display: flex;
.gutter {
width: var(--space-2);
background: linear-gradient(transparent, var(--gray-6) var(--space-4));
margin: 0 var(--space-4);
}
.content {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding-top: var(--space-4);
padding-bottom: var(--space-3);
}
}
</style>
<div class="api-description">
<div class="gutter"></div>
<div class="content">
<MarkdownCustom {body} />
<!-- {"see" in macros && <ApiMacroSee {locale} value={macros.see} />}
{"type" in macros && <ApiMacroType {locale} value={macros.type} />} -->
{
Object.entries(macroComponents).map(([macro, Component]) => {
if (!(macro in macros)) return null;
return macros[macro].map((value) => {
return <Component {locale} value={value} />;
});
})
}
</div>
</div>
@@ -1,8 +1,8 @@
---
import type { CollectionEntry } from "astro:content";
import { api } from "../core/api/dynamic";
import ApiDescription from "./ApiDescription.astro";
import { ApiTester } from "./ApiTester";
import { MarkdownCustom } from "./MarkdownCustom";
interface Props {
locale: string;
@@ -11,19 +11,17 @@ interface Props {
const { locale, api: apiData } = Astro.props;
const strings = await api.strings(locale);
const hasSlugs = apiData.id.includes("[");
const body = apiData.data.docs ?? strings.api.no_docs;
---
<style>
.container {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
</style>
<div class="container">
<ApiTester client:idle {locale} path={apiData.id} />
<MarkdownCustom body={apiData.data.docs ?? strings.api.no_docs} />
<ApiDescription {locale} {body} />
</div>
@@ -0,0 +1,36 @@
---
import { sluggify } from "@blitzkit/core";
import { Code } from "./Code";
import { Link } from "./Link";
import { Text } from "./Text";
interface Props {
value: string;
locale: string;
}
const bodyPattern = /^([a-z0-9-\/\.\[\]]+) (.+)$/;
const { value, locale } = Astro.props;
const match = value.match(bodyPattern);
if (match === null) {
throw new Error(`Invalid value for ApiMacroSee: ${value}`);
}
const path = match[1];
const message = match[2];
---
<Text>
<Text lowContrast weight="medium" color="tomato">
<Code>@see</Code>
</Text>
<Link underline="never" href={`#${sluggify(path)}`}>
<Code>{path}</Code>
</Link>
{message}
</Text>
@@ -0,0 +1,146 @@
---
import { existsSync } from "fs";
import { readFile } from "fs/promises";
import { Link } from "./Link";
import { api } from "../core/api/dynamic";
import HighlightedCodeBlock from "./HighlightedCodeBlock.astro";
import { Text } from "./Text";
import { ArrowBottomRightIcon } from "@radix-ui/react-icons";
import { Code } from "./Code";
interface Props {
value: string;
locale: string;
}
const { value, locale } = Astro.props;
const strings = await api.strings(locale);
const roots = ["src/protos", "../closed/protos"];
const lang = "proto";
const importPattern = /import "(.+)";/gm;
const messagePattern = /message .+ {\s*\n(.+\s*\n)+}/gm;
const explored = new Set<string>();
let messages: string[] = [];
await pullMessages(`${roots[0]}/${value}.proto`);
async function pullMessages(path: string) {
if (explored.has(path)) return;
explored.add(path);
const file = await readFile(path);
const content = file.toString();
const imports = content.matchAll(importPattern);
for (const _import of imports) {
for (const root of roots) {
const path = `${root}/${_import[1]}`;
if (existsSync(path)) {
await pullMessages(path);
break;
}
}
}
const matchedMessages = content.matchAll(messagePattern);
for (const message of matchedMessages) {
messages = [message[0], ...messages];
}
}
const content = messages.join("\n\n");
const indexPattern = /^( +).+\.(.+ = \d+;)$/m;
let stripped = "";
for (const line of content.split("\n")) {
const matches = line.match(indexPattern);
if (matches === null) {
stripped += `${line}\n`;
continue;
}
stripped += `${matches[1]}${matches[2]}\n`;
}
stripped = stripped.replaceAll(" ", " ");
---
<style>
.macro {
color: var(--gray-11);
display: flex;
align-items: center;
gap: var(--space-2);
}
.types {
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
&:not([data-expanded="true"]) {
max-height: 16rem;
}
&[data-expanded="true"] {
.overlay {
display: none;
}
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(transparent, var(--gray-1));
display: flex;
align-items: end;
justify-content: center;
}
}
</style>
<script>
const viewers = document.querySelectorAll(".view-full");
for (const viewer of viewers) {
viewer.addEventListener("click", () => {
viewer.parentElement!.parentElement!.dataset.expanded = "true";
});
}
</script>
<div class="macro">
<Text lowContrast weight="medium" color="tomato">
<Code>@types</Code>
</Text>
<ArrowBottomRightIcon />
</div>
<div class="types">
<HighlightedCodeBlock {lang} content={stripped} />
<div class="overlay">
<Link underline="always" href="#" className="view-full">
{strings.api.view_full}
</Link>
</div>
</div>
@@ -1,4 +1,6 @@
import { useState } from "react";
import { sluggify } from "@blitzkit/core";
import { PlayIcon } from "@radix-ui/react-icons";
import { Fragment, useState } from "react";
import {
LocaleProvider,
type LocaleAcceptorProps,
@@ -6,8 +8,8 @@ import {
import { useStrings } from "../../hooks/useStrings";
import { Button } from "../Button";
import { Code } from "../Code";
import { Heading } from "../Heading";
import { Link } from "../Link";
import { Text } from "../Text";
import { TextField } from "../TextField";
import styles from "./index.module.css";
@@ -39,19 +41,19 @@ function Content({ path }: Props) {
);
return (
<div className={styles.tester}>
<Heading size="3">
<div className={styles.tester} id={sluggify(path)}>
<Text weight="medium">
<Code className={styles.title}>
{path.split(slugPattern).map((text, index) => {
const slug = slugs[index - 1];
return (
<>
<Fragment key={index}>
{index > 0 && (
<TextField
variant="form"
className={styles.slug}
placeholder={`${slug}`}
monospace
placeholder={`[${slug}]`}
align="center"
autoWidth
onChange={(event) => {
@@ -63,14 +65,16 @@ function Content({ path }: Props) {
/>
)}
<span key={index}>{text}</span>
</>
</Fragment>
);
})}
</Code>
</Heading>
</Text>
<Link underline="never" href={href} target="_blank">
<Button>{strings.api.test}</Button>
<Button>
{strings.api.test} <PlayIcon />
</Button>
</Link>
</div>
);
@@ -1,3 +0,0 @@
.code {
font-family: var(--font-family-mono);
}
@@ -0,0 +1,16 @@
.code {
font: inherit;
font-family: var(--font-family-mono);
white-space: pre;
display: inline-block;
&[data-variant="solid"] {
background-color: var(--gray-3);
border-radius: var(--space-3);
padding: var(--space-4);
}
}
@@ -1,9 +1,17 @@
import type { ComponentProps } from "react";
import { classNames } from "../../core/ui/classNames";
import "./index.css";
import styles from "./index.module.css";
export interface CodeProps extends ComponentProps<"code"> {}
export interface CodeProps extends ComponentProps<"code"> {
variant?: "ghost" | "solid";
}
export function Code({ className, ...props }: CodeProps) {
return <code className={classNames("code", className)} {...props} />;
export function Code({ className, variant = "ghost", ...props }: CodeProps) {
return (
<code
data-variant={variant}
className={classNames(styles.code, className)}
{...props}
/>
);
}
@@ -0,0 +1,23 @@
---
import { all, createStarryNight } from "@wooorm/starry-night";
import { HighlightedCodeContent } from "./HighlightedCodeContent";
interface Props {
lang: string;
content: string;
}
const { lang, content } = Astro.props;
const starryNight = await createStarryNight(all);
const scope = starryNight.flagToScope(lang);
if (scope === undefined && lang.length > 0) {
throw new Error(`Unsupported language: ${lang}`);
}
const tree =
lang.length === 0 ? content : starryNight.highlight(content, scope!);
---
<HighlightedCodeContent {tree} />
@@ -0,0 +1,17 @@
import type { Root } from "hast";
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { Code } from "./Code";
interface Props {
tree: Root | string;
}
export function HighlightedCodeContent({ tree }: Props) {
const node =
typeof tree === "string"
? tree
: toJsxRuntime(tree, { Fragment, jsx, jsxs });
return <Code variant="solid" children={node} />;
}
@@ -1,7 +1,7 @@
.link {
text-decoration-color: currentColor;
display: flex;
display: inline;
align-items: center;
gap: var(--space-3);
@@ -0,0 +1,3 @@
<style is:global>
@import "@wooorm/starry-night/style/dark";
</style>
@@ -2,10 +2,6 @@
font-size: var(--font-size-regular);
color: var(--gray-12);
&[data-low-contrast="true"] {
color: var(--white-a9);
}
&[data-size="minor"] {
font-size: var(--font-size-minor);
}
@@ -1,12 +1,14 @@
import type { ComponentProps } from "react";
import { classNames } from "../../core/ui/classNames";
import "./index.css";
import type { Color } from "../../core/ui/color";
import styles from "./index.module.css";
export interface TextProps extends ComponentProps<"span"> {
weight?: "light" | "regular" | "medium" | "bold" | "black";
lowContrast?: boolean;
size?: "regular" | "minor" | "major";
align?: "left" | "center" | "right";
color?: Color;
}
export function Text({
@@ -16,15 +18,17 @@ export function Text({
weight,
style,
align,
color = "gray",
...props
}: TextProps) {
return (
<span
className={classNames("text", className)}
className={classNames(styles.text, className)}
data-low-contrast={lowContrast}
data-size={size}
data-weight={weight}
data-align={align === "left" ? undefined : align}
style={{ color: `var(--${color}-${lowContrast ? 10 : 12})` }}
{...props}
/>
);
@@ -5,15 +5,30 @@
gap: var(--space-3);
border-radius: var(--max-size);
background-color: var(--gray-1);
box-shadow: var(--shadow-interactive), var(--shadow-cavity);
cursor: text;
font-size: var(--font-size-regular);
font: inherit;
color: var(--gray-11);
&[data-variant="bubble"] {
border-radius: var(--max-size);
background-color: var(--gray-1);
box-shadow: var(--shadow-interactive), var(--shadow-cavity);
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);
@@ -34,7 +49,7 @@
color: var(--gray-9);
input::placeholder {
color: var(--gray-9);
color: var(--gray-6);
}
}
@@ -51,14 +66,12 @@
border: none;
cursor: inherit;
font-size: inherit;
font-weight: medium;
padding: var(--space-3) var(--space-4);
font: inherit;
color: var(--gray-12);
&::placeholder {
color: var(--gray-11);
color: var(--gray-9);
}
&:focus {
@@ -71,9 +84,5 @@
&[data-align="right"] {
text-align: right;
}
&[data-monospace="true"] {
font-family: var(--font-family-mono);
}
}
}
@@ -13,8 +13,8 @@ import "./index.css";
export interface TextFieldProps extends ComponentProps<"input"> {
children?: ReactNode;
align?: "left" | "center" | "right";
monospace?: boolean;
autoWidth?: boolean;
variant?: "bubble" | "form";
}
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
@@ -22,13 +22,13 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
{
children,
className,
monospace,
autoWidth,
placeholder,
align,
disabled,
value,
onChange,
variant = "bubble",
...props
},
ref,
@@ -54,11 +54,11 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
className={classNames("text-field", className)}
data-has-icon={children !== undefined}
data-disabled={disabled}
data-variant={variant}
>
{children}
<input
data-align={align === "left" ? undefined : align}
data-monospace={monospace}
disabled={disabled}
ref={input}
onChange={handleChange}
@@ -26,13 +26,13 @@ const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
display: flex;
justify-content: center;
gap: var(--space-4);
padding: var(--space-5) var(--space-4);
padding: var(--space-5) var(--space-5);
box-sizing: border-box;
backdrop-filter: var(--blur-tint);
@media (width > 40rem) {
padding: var(--space-6) var(--space-4);
padding: var(--space-6) var(--space-5);
}
.centered {
@@ -2,8 +2,10 @@
import "@radix-ui/colors/amber-dark.css";
import { getCollection } from "astro:content";
import ApiGroup from "../../../components/ApiGroup.astro";
import ColorScale from "../../../components/ColorScale.astro";
import { Heading } from "../../../components/Heading";
import { Section } from "../../../components/Section";
import SyntaxStyles from "../../../components/SyntaxStyles.astro";
import ToolBanner from "../../../components/ToolBanner.astro";
import { api } from "../../../core/api/dynamic";
import { tools } from "../../../core/tools/tools";
@@ -24,10 +26,13 @@ paths.sort((a, b) => a.path.localeCompare(b.path));
---
<BaseLayout {locale} accent="brown">
<SyntaxStyles slot="head" />
<ColorScale color="tomato" slot="head" />
<ToolBanner {locale} tool={tools.api} slot="pre-app" />
<Section>
<Heading>{strings.api.json}</Heading>
<Heading size="2">{strings.api.json}</Heading>
<ApiGroup
{locale}
apis={collection.filter((api) => api.id.endsWith(".json"))}
@@ -35,7 +40,7 @@ paths.sort((a, b) => a.path.localeCompare(b.path));
</Section>
<Section>
<Heading>{strings.api.pb}</Heading>
<Heading size="2">{strings.api.pb}</Heading>
<ApiGroup
{locale}
apis={collection.filter((api) => api.id.endsWith(".pb"))}
@@ -43,7 +48,7 @@ paths.sort((a, b) => a.path.localeCompare(b.path));
</Section>
<Section>
<Heading>{strings.api.media}</Heading>
<Heading size="2">{strings.api.media}</Heading>
<ApiGroup
{locale}
apis={collection.filter(
@@ -10,9 +10,12 @@ export const getStaticPaths = (async () => {
}) satisfies GetStaticPaths;
/**
* Returns data on an avatar based on its id. @see avatars/list.json for a list
* of all avatar ids.
* Returns data on an avatar based on its `id`. Do not use the `.webp` URLs
* from the returned data as they point to Wargaming's servers which may update
* over time, out of sync with BlitzKit.
*
* @see avatars/list.json for all avatar ids.
* @see avatars/[id].webp for avatar images.
* @type avatar
*/
export async function GET({ params }: APIContext<never, { id: string }>) {