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:
parent
167405aed0
commit
e00ea8eb11
204 changed files with 6469 additions and 833 deletions
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 })}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { TFunction } from 'i18next'
|
||||
import type { TFunction } from '@shared/i18n/react'
|
||||
|
||||
const RU_PLURAL = new Intl.PluralRules('ru')
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue