feat: frontend perf and a11y pass, plus mod visuals, hero main menu, Simple Voice Chat and vanilla chat autocomplete

Frontend:
- replace the i18next stack with a small typed i18n module
- hero clouds removed, llms.txt, HUD widget ARIA fixes and tests

Mod:
- TargetESP constellation mode; tracker and colors extracted
- JumpCircles: 7 new shapes, shared animation and color pipeline
- KillEffect blood mode with Burst/Fountain/Ring/Spray styles
- main menu: menuLayout setting (orbs/hero) and hero screen
- Simple Voice Chat soft integration: VoiceChat module and HUD panel
- renderer/quick: static Draw facade with glass outline
- vanilla chat: Baritone-style autocomplete popup for the % command prefix
This commit is contained in:
loki5512344 2026-10-01 16:22:05 +02:00
parent 167405aed0
commit e00ea8eb11
Signed by: boba
GPG key ID: 253067914055423B
204 changed files with 6469 additions and 833 deletions

View file

@ -1,5 +1,5 @@
import { api } from '@shared/api/client'
import type { TFunction } from 'i18next'
import type { TFunction } from '@shared/i18n/react'
export type DeviceLink = { id: string; linked_at: string; last_seen: string | null }

View file

@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Link } from 'react-router'
import { Button } from '@shared/ui/Button'
import { listDevices, revokeDevice, type DeviceLink } from '../api'

View file

@ -1,5 +1,5 @@
import { useCallback, useRef, useState, type DragEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { useNavigate } from 'react-router'
import { Button } from '@shared/ui/Button'
import { useSession } from '@features/auth/session'

View file

@ -1,5 +1,5 @@
import { useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Button } from '@shared/ui/Button'
import { clampPan, clampZoom, coverScale, MAX_ZOOM, MIN_ZOOM, sourceRect } from './cropMath'

View file

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { listAvatarHistory, type AvatarHistoryEntry } from '@features/account/api'
type Props = {

View file

@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useCallback, useEffect, useId, useState, type ChangeEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Notice } from '@shared/ui/Notice'
import { useSession } from '@features/auth/session'
import {

View file

@ -1,6 +1,6 @@
import { screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { getI18n } from 'react-i18next'
import { getI18n } from '@shared/i18n/react'
import { expect, test } from 'vitest'
import { checkAvatarFile } from '../api'
import { DeviceList } from '@features/account/components/DeviceList'

View file

@ -1,4 +1,4 @@
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Navigate, Outlet, useLocation } from 'react-router'
import { useSession } from './session'

View file

@ -1,5 +1,5 @@
import { useState, type FormEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { useLocation, useNavigate } from 'react-router'
import { describeError } from '@shared/api/errors'
import { Button } from '@shared/ui/Button'

View file

@ -1,5 +1,5 @@
import { useState, type FormEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { useLocation, useNavigate } from 'react-router'
import { describeError } from '@shared/api/errors'
import { Button } from '@shared/ui/Button'

View file

@ -1,4 +1,4 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@shared/i18n/react'
export function validateEmail(email: string, t: TFunction<'auth'>): string | undefined {
const v = email.trim()

View file

@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { prefersReducedMotion } from '@shared/motion/reducedMotion'
import type { ThemeEntry } from '../themes/types'
import { useActiveTheme } from '../themes/useActiveTheme'

View file

@ -1,5 +1,5 @@
import type { Dispatch } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import type { GuiAction, GuiState } from '../demo'
import { Icon } from '../parts/Glyphs'
import { type Category, CATEGORIES } from '../parts/modules'
@ -22,7 +22,7 @@ function Panel({ category, state, dispatch }: { category: Category } & Props) {
const { t } = useTranslation('clickgui')
const titleId = `lv-gui-panel-${category.id}`
return (
<section role="listitem" className="lv-gui-panel" data-category={category.id} aria-labelledby={titleId}>
<div role="listitem" className="lv-gui-panel" data-category={category.id} aria-labelledby={titleId}>
<header className="lv-gui-panel-head">
<h2 id={titleId} className="lv-gui-panel-title">
{category.name}
@ -51,6 +51,6 @@ function Panel({ category, state, dispatch }: { category: Category } & Props) {
)
})}
</ul>
</section>
</div>
)
}

View file

@ -1,5 +1,5 @@
import type { CSSProperties } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { PRESETS } from '@features/themes/presets.generated'
import { argbToCss, type GradientSpec, type ThemeColors, type ThemeEntry } from '@features/themes/types'
import { Icon } from '../parts/Glyphs'

View file

@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Button } from '@shared/ui/Button'
import { TextField } from '@shared/ui/text-field/TextField'
import { publish } from '../api'

View file

@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Button } from '@shared/ui/Button'
import { Notice } from '@shared/ui/Notice'
import { ShareCode } from '@shared/ui/share-code/ShareCode'

View file

@ -1,6 +1,6 @@
import { useMutation } from '@tanstack/react-query'
import { useState, type FormEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { describeError } from '@shared/api/errors'
import { Button } from '@shared/ui/Button'
import { Notice } from '@shared/ui/Notice'

View file

@ -1,4 +1,4 @@
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { DOWNLOAD_URL, formatSize } from './api'
import '../landing/styles/landing.css'

View file

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Markdown } from '@shared/ui/Markdown'
import { fetchReleases } from './api'

View file

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { prefersReducedMotion } from '@shared/motion/reducedMotion'
import { ButtonLink } from '@shared/ui/Button'
import { useSession } from '../auth/session'

View file

@ -1,4 +1,4 @@
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Link } from 'react-router'
import '@features/landing/styles/sections.css'

View file

@ -119,15 +119,6 @@ function drawSky(ctx: CanvasRenderingContext2D, w: number, h: number) {
ctx.fillRect(sx - sun / 2, sy - sun / 2, sun, sun)
ctx.fillStyle = '#ffe7b8'
ctx.fillRect(sx - sun / 4, sy - sun / 4, sun / 2, sun / 2)
// Blocky clouds.
for (let c = 0; c < 7; c++) {
const cx = rnd() * w
const cy = h * (0.12 + rnd() * 0.3)
const u = 14
ctx.fillStyle = `rgba(${200 + rnd() * 40}, ${120 + rnd() * 40}, ${170 + rnd() * 40}, 0.2)`
const len = 4 + Math.floor(rnd() * 7)
for (let i = 0; i < len; i++) ctx.fillRect(cx + i * u, cy + (i % 3 === 0 ? -u : 0), u, u * (i % 3 === 0 ? 2 : 1))
}
}
/**

View file

@ -1,4 +1,4 @@
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Link } from 'react-router'
import { GITHUB_RELEASES_URL } from '@features/download/api'
import '@features/landing/styles/landing.css'

View file

@ -1,5 +1,5 @@
import { useLayoutEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Button } from '@shared/ui/Button'
import { themeToCssVars } from '@features/clickgui/themeVars'
import { parseArgb } from '@features/themes/types'
@ -97,7 +97,7 @@ export function HudPlayground() {
ref={(el) => {
widgetRefs.current[w.id] = el
}}
role="button"
role="group"
tabIndex={0}
title={w.label}
aria-label={t('hud.dragAria', { name: w.label })}

View file

@ -1,4 +1,4 @@
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { CATEGORIES } from '@features/clickgui/parts/modules'
import '@features/landing/styles/landing.css'

View file

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Link } from 'react-router'
import { browse } from '@features/showcase/api'
import '@features/landing/styles/sections.css'

View file

@ -1,5 +1,5 @@
import { lazy, Suspense, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Link } from 'react-router'
import { ClickGuiWindow } from '@features/clickgui/ClickGuiWindow'
import { PRESETS } from '@features/themes/presets.generated'

View file

@ -1,5 +1,5 @@
import { type MouseEvent, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { ButtonLink } from '@shared/ui/Button'
import { prefersReducedMotion } from '@shared/motion/reducedMotion'
import { RollText } from '@shared/motion/RollText'

View file

@ -43,7 +43,7 @@ test('picking a swatch repaints the theme section: pressed state and the big the
test('holding a HUD widget switches the screen into edit mode until it is released', () => {
reduceMotion()
withTheme(<HudPlayground />)
const widget = screen.getByRole('button', { name: /FPS/ })
const widget = screen.getByRole('group', { name: /FPS/ })
widget.setPointerCapture = () => {}
const grid = document.querySelector('.lv-hud-grid')
expect(grid).toHaveAttribute('data-on', 'false')
@ -60,11 +60,11 @@ test('holding a HUD widget switches the screen into edit mode until it is releas
test('"Put it back" restores the HUD after moving a widget', () => {
reduceMotion()
withTheme(<HudPlayground />)
const start = screen.getByRole('button', { name: /FPS/ }).style.left
fireEvent.keyDown(screen.getByRole('button', { name: /FPS/ }), { key: 'ArrowRight' })
expect(screen.getByRole('button', { name: /FPS/ }).style.left).not.toBe(start)
const start = screen.getByRole('group', { name: /FPS/ }).style.left
fireEvent.keyDown(screen.getByRole('group', { name: /FPS/ }), { key: 'ArrowRight' })
expect(screen.getByRole('group', { name: /FPS/ }).style.left).not.toBe(start)
fireEvent.click(screen.getByRole('button', { name: 'Вернуть как было' }))
expect(screen.getByRole('button', { name: /FPS/ }).style.left).toBe(start)
expect(screen.getByRole('group', { name: /FPS/ }).style.left).toBe(start)
})
test('module wall shows how many modules each category has; Visuals has the 52 the headline promises', () => {

View file

@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import { expect, test, vi } from 'vitest'
import { I18nextProvider } from 'react-i18next'
import { I18nProvider } from '@shared/i18n/I18nProvider'
import { CommandHero } from '../CommandHero'
import { Hero } from '../sections/hero/Hero'
import { createMemoryRouter, RouterProvider } from 'react-router'
@ -29,13 +29,13 @@ test('english locale renders the landing hero headline', async () => {
const i18n = initI18n('en')
const router = createMemoryRouter([{ path: '/', element: <Hero /> }])
render(
<I18nextProvider i18n={i18n}>
<I18nProvider i18n={i18n}>
<QueryClientProvider client={new QueryClient()}>
<SessionProvider>
<RouterProvider router={router} />
</SessionProvider>
</QueryClientProvider>
</I18nextProvider>,
</I18nProvider>,
)
expect(await screen.findByRole('heading', { level: 1 })).toHaveTextContent("you'll fall in love with")
})
@ -49,11 +49,11 @@ test('cloud section speaks in player terms in both languages', async () => {
] as const) {
const router = createMemoryRouter([{ path: '/', element: <CommandHero /> }])
const { unmount } = render(
<I18nextProvider i18n={initI18n(lng)}>
<I18nProvider i18n={initI18n(lng)}>
<QueryClientProvider client={new QueryClient()}>
<SessionProvider><RouterProvider router={router} /></SessionProvider>
</QueryClientProvider>
</I18nextProvider>,
</I18nProvider>,
)
expect(await screen.findByRole('heading', { level: 2 })).toHaveTextContent(title)
unmount()
@ -66,11 +66,11 @@ test('hero download CTA goes to the page with both download buttons', async () =
const router = createMemoryRouter([{ path: '/', element: <Hero /> }])
// Explicit ru: an earlier test in this file re-inits the default instance in English.
render(
<I18nextProvider i18n={initI18n('ru')}>
<I18nProvider i18n={initI18n('ru')}>
<QueryClientProvider client={new QueryClient()}>
<SessionProvider><RouterProvider router={router} /></SessionProvider>
</QueryClientProvider>
</I18nextProvider>,
</I18nProvider>,
)
const cta = await screen.findByRole('link', { name: 'Скачать бесплатно' })
expect(cta).toHaveAttribute('href', '/download')

View file

@ -26,17 +26,17 @@ afterEach(() => vi.unstubAllGlobals())
test('HudWidget moves by 8px with ArrowRight', () => {
reduceMotion()
withQuery(<HudPlayground />)
const widget = screen.getByRole('button', { name: /FPS/ })
const widget = screen.getByRole('group', { name: /FPS/ })
const before = Number.parseInt(widget.style.left, 10)
fireEvent.keyDown(widget, { key: 'ArrowRight' })
expect(Number.parseInt(screen.getByRole('button', { name: /FPS/ }).style.left, 10)).toBe(before + 8)
expect(Number.parseInt(screen.getByRole('group', { name: /FPS/ }).style.left, 10)).toBe(before + 8)
})
test('HudWidget stays inside the playfield bounds', () => {
reduceMotion()
withQuery(<HudPlayground />)
for (let i = 0; i < 200; i++) fireEvent.keyDown(screen.getByRole('button', { name: /FPS/ }), { key: 'ArrowRight' })
const left = Number.parseInt(screen.getByRole('button', { name: /FPS/ }).style.left, 10)
for (let i = 0; i < 200; i++) fireEvent.keyDown(screen.getByRole('group', { name: /FPS/ }), { key: 'ArrowRight' })
const left = Number.parseInt(screen.getByRole('group', { name: /FPS/ }).style.left, 10)
expect(left).toBeGreaterThanOrEqual(0)
expect(left).toBeLessThanOrEqual(680)
})

View file

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { ApiError } from '@shared/api/client'
import { Notice } from '@shared/ui/Notice'
import { ListingList } from '@features/showcase/components/ListingList'

View file

@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Link, useLocation } from 'react-router'
import { Notice } from '@shared/ui/Notice'
import { ApiError } from '@shared/api/client'

View file

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Link } from 'react-router'
import { Notice } from '@shared/ui/Notice'
import { getListing } from '../api'

View file

@ -1,4 +1,4 @@
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { Link } from 'react-router'
import { copiesText } from '../copies'
import type { Listing } from '../types'

View file

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { useSearchParams } from 'react-router'
import { Button } from '@shared/ui/Button'
import { Notice } from '@shared/ui/Notice'

View file

@ -1,4 +1,4 @@
import type { TFunction } from 'i18next'
import type { TFunction } from '@shared/i18n/react'
const RU_PLURAL = new Intl.PluralRules('ru')

View file

@ -1,5 +1,5 @@
import { type ReactNode, startTransition, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { themeToCssVars } from '@features/clickgui/themeVars'
import { PRESETS } from '../presets.generated'
import type { ThemeEntry } from '../types'

View file

@ -1,5 +1,5 @@
import { type CSSProperties, memo, useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { argbToCss, parseArgb } from '../../types'
import './fields.css'

View file

@ -1,5 +1,5 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { cssAngle } from '@features/clickgui/themeVars'
import { argbToCss, type GradientSpec } from '../../types'
import { ColorField } from './ColorField'

View file

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { parseArgb, type ThemeEntry } from '../../types'
import { fromProfileJson, toProfileJson, toShareHash } from '../model/codec'

View file

@ -1,5 +1,5 @@
import { type KeyboardEvent, useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import type { GradientSpec, ThemeColors, ThemeEntry } from '../../types'
import { ColorField } from '../fields/ColorField'
import { GradientField } from '../fields/GradientField'

View file

@ -1,4 +1,4 @@
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import type { ThemeEntry } from '../../types'
type History = { undo: () => void; redo: () => void; reset: () => void; resetName: string; canUndo: boolean; canRedo: boolean }

View file

@ -1,5 +1,5 @@
import type { CSSProperties } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@shared/i18n/react'
import { themeToCssVars } from '@features/clickgui/themeVars'
import { PRESETS } from '../../presets.generated'