feat: webcore/old-web visual redesign
CI / Go (build + vet + test) (push) Canceled after 0s
CI / Web (build + lint) (push) Canceled after 0s

This commit is contained in:
vantablackTea
2026-09-28 13:02:58 +03:00
parent 5e30761742
commit 61183d3f89
17 changed files with 861 additions and 609 deletions
+47 -52
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import Avatar from './Avatar'
import { IconClose, IconSearch, IconUserPlus, IconUsers } from './icons'
import { IconSearch, IconUserPlus, IconUsers } from './icons'
import { api } from '../lib/api'
import { userTitle } from '../lib/format'
import type { Contact, User } from '../lib/types'
@@ -145,43 +145,32 @@ export default function NewChatDialog({
const canCreate = groupName.trim().length > 0 && selectedIds.size > 0 && !creating
return (
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/60 p-0 backdrop-blur-sm sm:items-center sm:p-6">
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/50 p-0 sm:items-center sm:p-4">
<button type="button" aria-label="Закрыть" onClick={onClose} className="absolute inset-0 cursor-default" />
<div
role="dialog"
aria-modal="true"
aria-labelledby="new-chat-title"
className="relative flex max-h-[85vh] w-full max-w-md animate-rise flex-col overflow-hidden rounded-t-3xl border border-line bg-panel shadow-2xl sm:rounded-3xl"
className="bevel relative flex max-h-[85vh] w-full max-w-md animate-rise flex-col bg-panel shadow-[4px_4px_0_rgba(0,0,0,0.4)]"
>
<div className="flex items-center justify-between border-b border-line/80 px-5 py-4">
<div>
<h2 id="new-chat-title" className="font-display text-base font-semibold text-fg">
Новый чат
</h2>
<p className="text-xs text-muted">
{mode === 'dm' ? 'Найдите человека или выберите из контактов' : 'Соберите группу и добавьте участников'}
</p>
</div>
<button
type="button"
onClick={onClose}
aria-label="Закрыть"
className="inline-flex h-9 w-9 items-center justify-center rounded-xl text-muted transition hover:bg-surface-2 hover:text-fg"
>
<IconClose className="h-5 w-5" />
<div className="win-title flex shrink-0 items-center gap-1.5 px-1.5 py-1">
<span className="flex-1 truncate text-xs font-bold tracking-wide">Новый чат</span>
<button type="button" onClick={onClose} aria-label="Закрыть" className="win-btn">
×
</button>
</div>
<div className="px-5 pt-4">
<div className="mb-3 grid grid-cols-2 gap-1 rounded-xl border border-line bg-surface/60 p-1">
<div className="px-2 pt-2">
<p className="mb-2 text-[11px] text-muted">
{mode === 'dm' ? 'Найдите человека или выберите из контактов' : 'Соберите группу и добавьте участников'}
</p>
<div className="mb-2 grid grid-cols-2 gap-1">
{(['dm', 'group'] as const).map((value) => (
<button
key={value}
type="button"
onClick={() => setMode(value)}
className={`rounded-lg px-3 py-1.5 text-xs font-semibold transition ${
mode === value ? 'bg-accent-ghost text-accent ring-1 ring-accent/30' : 'text-muted hover:text-fg'
}`}
className={`px-3 py-1.5 text-xs font-bold ${mode === value ? 'bevel-in bg-surface-2' : 'retro-btn'}`}
>
{value === 'dm' ? 'Личный чат' : 'Группа'}
</button>
@@ -189,8 +178,8 @@ export default function NewChatDialog({
</div>
{mode === 'group' && (
<div className="relative mb-3">
<IconUsers className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-faint" />
<div className="relative mb-2">
<IconUsers className="pointer-events-none absolute top-1/2 left-2 h-3.5 w-3.5 -translate-y-1/2 text-surface-3" />
<label htmlFor="group-name" className="sr-only">
Название группы
</label>
@@ -201,13 +190,13 @@ export default function NewChatDialog({
onChange={(event) => setGroupName(event.target.value)}
placeholder="Название группы"
maxLength={80}
className="w-full rounded-xl border border-line bg-surface/70 py-2.5 pr-3 pl-9 text-sm text-fg placeholder:text-faint focus:border-accent/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/20"
className="retro-input w-full py-1.5 pr-2 pl-7 text-xs"
/>
</div>
)}
<div className="relative">
<IconSearch className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-faint" />
<IconSearch className="pointer-events-none absolute top-1/2 left-2 h-3.5 w-3.5 -translate-y-1/2 text-surface-3" />
<label htmlFor="new-chat-search" className="sr-only">
Поиск пользователей
</label>
@@ -218,28 +207,28 @@ export default function NewChatDialog({
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Имя или @username"
className="w-full rounded-xl border border-line bg-surface/70 py-2.5 pr-3 pl-9 text-sm text-fg placeholder:text-faint focus:border-accent/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/20"
className="retro-input w-full py-1.5 pr-2 pl-7 text-xs"
/>
</div>
{error && <p className="mt-2 text-xs text-danger">{error}</p>}
{error && <p className="mt-1.5 text-[11px] text-danger">{error}</p>}
</div>
<div className="scrollbar-slim flex-1 overflow-y-auto px-3 py-3">
<div className="scrollbar-slim flex-1 overflow-y-auto px-1.5 py-2">
{(loadingContacts && trimmed.length === 0) || (searching && list.length === 0) ? (
<div className="space-y-1.5 p-1">
<div className="space-y-1 p-1">
{[0, 1, 2].map((row) => (
<div key={row} className="flex items-center gap-3 rounded-xl p-2.5">
<span className="h-10 w-10 animate-pulse-soft rounded-full bg-surface-2" />
<span className="h-3 w-1/2 animate-pulse-soft rounded-full bg-surface-2" />
<div key={row} className="flex items-center gap-2 p-1.5">
<span className="h-9 w-9 animate-pulse-soft bg-surface-2" />
<span className="h-2.5 w-1/2 animate-pulse-soft bg-surface-2" />
</div>
))}
</div>
) : list.length === 0 ? (
<div className="px-6 py-12 text-center text-sm text-muted">
<div className="px-4 py-10 text-center text-xs text-muted">
{trimmed.length > 0 ? 'Пользователи не найдены' : 'Список контактов пуст'}
</div>
) : (
<ul className="space-y-0.5">
<ul>
{list.map((user) => {
const isContact = contactIds.has(user.id)
const busy = busyId === user.id
@@ -247,8 +236,8 @@ export default function NewChatDialog({
return (
<li key={user.id}>
<div
className={`flex items-center gap-3 rounded-xl px-2.5 py-2 transition ${
mode === 'group' && selected ? 'bg-accent-ghost ring-1 ring-accent/30' : 'hover:bg-surface-2/70'
className={`flex items-center gap-2 px-1.5 py-1.5 ${
mode === 'group' && selected ? 'bevel-in bg-accent-ghost' : 'hover:bg-surface-2'
}`}
>
{mode === 'group' && (
@@ -257,19 +246,25 @@ export default function NewChatDialog({
onClick={() => toggleMember(user.id)}
aria-pressed={selected}
aria-label={selected ? 'Убрать из группы' : 'Добавить в группу'}
className={`inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition ${
selected ? 'border-accent bg-accent text-white' : 'border-line bg-surface text-transparent'
className={`inline-flex h-4 w-4 shrink-0 items-center justify-center ${
selected ? 'bevel-in bg-accent text-white' : 'bevel-in bg-surface text-transparent'
}`}
>
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" aria-hidden="true">
<path d="m5 12.5 4.5 4.5L19 7" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
<svg viewBox="0 0 24 24" className="h-3 w-3" fill="none" aria-hidden="true">
<path
d="m5 12.5 4.5 4.5L19 7"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="square"
strokeLinejoin="miter"
/>
</svg>
</button>
)}
<Avatar name={userTitle(user)} seed={user.id} size="md" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold text-fg">{userTitle(user)}</p>
<p className="truncate text-xs text-muted">@{user.username}</p>
<p className="truncate text-xs font-bold text-fg">{userTitle(user)}</p>
<p className="truncate font-mono text-[10px] text-muted">@{user.username}</p>
</div>
{mode === 'dm' && (
<>
@@ -278,9 +273,9 @@ export default function NewChatDialog({
type="button"
onClick={() => void handleAdd(user)}
disabled={busy}
className="inline-flex items-center gap-1 rounded-lg border border-line px-2.5 py-1.5 text-xs font-medium text-muted transition hover:border-accent/50 hover:text-accent disabled:opacity-50"
className="retro-btn inline-flex items-center gap-1 px-2 py-1 text-[11px] font-bold"
>
<IconUserPlus className="h-4 w-4" />
<IconUserPlus className="h-3.5 w-3.5" />
Добавить
</button>
)}
@@ -288,7 +283,7 @@ export default function NewChatDialog({
type="button"
onClick={() => void handleStart(user)}
disabled={busy}
className="rounded-lg bg-gradient-to-br from-accent to-accent-strong px-3 py-1.5 text-xs font-semibold text-white transition hover:brightness-110 disabled:opacity-50"
className="retro-btn-primary px-2.5 py-1 text-[11px] font-bold"
>
{busy ? '…' : 'Написать'}
</button>
@@ -303,15 +298,15 @@ export default function NewChatDialog({
</div>
{mode === 'group' && (
<div className="flex items-center justify-between gap-3 border-t border-line/80 px-5 py-3">
<span className="text-xs text-muted">
Выбрано: <span className="font-semibold text-fg">{selectedIds.size}</span>
<div className="flex items-center justify-between gap-2 border-t-2 border-surface-3 px-2 py-2">
<span className="text-[11px] text-muted">
Выбрано: <span className="font-mono font-bold text-fg">{selectedIds.size}</span>
</span>
<button
type="button"
onClick={() => void handleCreateGroup()}
disabled={!canCreate}
className="rounded-xl bg-gradient-to-br from-accent to-accent-strong px-4 py-2 text-sm font-semibold text-white transition enabled:hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-40"
className="retro-btn-primary px-3 py-1.5 text-xs font-bold"
>
{creating ? 'Создание…' : 'Создать группу'}
</button>