feat(design-preview): привести макет к дизайн-системе shnt Ops
Шрифты Geist и Geist Mono (только пока смонтирован макет), плашки статуса с глифом и точкой в первой колонке, счётчики в видах таблиц с запоминанием вида, закреплённые пункты меню, автосворачивание меню до 1100px, индикатор свежести в шапке, вызов палитры кнопкой только на узких экранах, главная кнопка в Sheet и предупреждение «Закрыть без сохранения». Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bd69b27e5c
commit
67a0510eeb
15 files changed
+292
-66
No files matched your search
@@ -16,6 +16,8 @@
|
|||||||
"@authportal/shared": "workspace:*",
|
"@authportal/shared": "workspace:*",
|
||||||
"@authportal/ui": "workspace:*",
|
"@authportal/ui": "workspace:*",
|
||||||
"@base-ui/react": "^1.5.0",
|
"@base-ui/react": "^1.5.0",
|
||||||
|
"@fontsource-variable/geist": "^5.3.0",
|
||||||
|
"@fontsource-variable/geist-mono": "^5.3.0",
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.4.0",
|
||||||
"@simplewebauthn/browser": "^13.3.0",
|
"@simplewebauthn/browser": "^13.3.0",
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
|
|||||||
@@ -181,7 +181,6 @@ export function AccessDialog({
|
|||||||
Отмена
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
disabled={lines.length === 0}
|
disabled={lines.length === 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSave(next)
|
onSave(next)
|
||||||
|
|||||||
@@ -35,9 +35,14 @@ import {
|
|||||||
import type { Tone } from './mock'
|
import type { Tone } from './mock'
|
||||||
import { TONE_BADGE } from './tone'
|
import { TONE_BADGE } from './tone'
|
||||||
|
|
||||||
|
const TONE_GLYPH: Record<Tone, string> = { ok: '●', wn: '▲', er: '✕', mu: '○', in: '' }
|
||||||
|
|
||||||
|
/** Плашка статуса: слово + глиф (● ▲ ✕ ○), чтобы цвет не был единственным признаком. */
|
||||||
export function StatusBadge({ tone, children }: { tone: Tone; children: ReactNode }) {
|
export function StatusBadge({ tone, children }: { tone: Tone; children: ReactNode }) {
|
||||||
|
const glyph = TONE_GLYPH[tone]
|
||||||
return (
|
return (
|
||||||
<Badge variant={TONE_BADGE[tone]} size="sm" className="whitespace-nowrap">
|
<Badge variant={TONE_BADGE[tone]} size="sm" className="whitespace-nowrap">
|
||||||
|
{glyph ? <span aria-hidden className="text-[0.55rem]">{glyph}</span> : null}
|
||||||
{children}
|
{children}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
@@ -122,8 +127,21 @@ export function PreviewSheet({
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
footer?: ReactNode
|
footer?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
const [touched, setTouched] = useState(false)
|
||||||
|
const [guard, setGuard] = useState(false)
|
||||||
|
|
||||||
|
function requestChange(next: boolean) {
|
||||||
|
if (!next && touched) {
|
||||||
|
setGuard(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!next) setTouched(false)
|
||||||
|
onOpenChange(next)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<>
|
||||||
|
<Sheet open={open} onOpenChange={requestChange}>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
side="right"
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
@@ -150,12 +168,53 @@ export function PreviewSheet({
|
|||||||
{description}
|
{description}
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-4 py-4">{children}</div>
|
<div
|
||||||
|
className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-4 py-4"
|
||||||
|
onInput={() => setTouched(true)}
|
||||||
|
onClick={(e) => {
|
||||||
|
if ((e.target as HTMLElement).closest('[role=switch]')) setTouched(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
{footer ? (
|
{footer ? (
|
||||||
<SheetFooter className="flex-row justify-end gap-2 border-t px-4 py-3">{footer}</SheetFooter>
|
<SheetFooter
|
||||||
|
className="flex-row flex-wrap justify-end gap-2 border-t px-4 py-3"
|
||||||
|
onClickCapture={(e) => {
|
||||||
|
// «Закрыть» и «Отмена» при изменённой форме тоже спрашивают подтверждение.
|
||||||
|
const label = (e.target as HTMLElement).closest('button')?.textContent?.trim()
|
||||||
|
if (touched && (label === 'Закрыть' || label === 'Отмена')) {
|
||||||
|
e.stopPropagation()
|
||||||
|
setGuard(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>{footer}</SheetFooter>
|
||||||
) : null}
|
) : null}
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
<AlertDialog open={guard} onOpenChange={setGuard}>
|
||||||
|
<AlertDialogContent size="sm">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Закрыть без сохранения?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>Изменения в форме будут потеряны.</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Остаться</AlertDialogCancel>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
setGuard(false)
|
||||||
|
setTouched(false)
|
||||||
|
onOpenChange(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Закрыть без сохранения
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
@import '@fontsource-variable/geist';
|
||||||
|
@import '@fontsource-variable/geist-mono';
|
||||||
|
|
||||||
/* Плотность и контраст макета. Атрибуты ставит PreviewProvider и снимает при выходе. */
|
/* Плотность и контраст макета. Атрибуты ставит PreviewProvider и снимает при выходе. */
|
||||||
html[data-preview-density='compact'] [data-slot='data-grid-table'] :is(th, td) {
|
html[data-preview-density='compact'] [data-slot='data-grid-table'] :is(th, td) {
|
||||||
padding-block: 0.25rem;
|
padding-block: 0.25rem;
|
||||||
@@ -12,3 +15,14 @@ html[data-preview-contrast='high'] {
|
|||||||
--border: color-mix(in oklch, var(--foreground) 32%, var(--background));
|
--border: color-mix(in oklch, var(--foreground) 32%, var(--background));
|
||||||
--input: color-mix(in oklch, var(--foreground) 40%, var(--background));
|
--input: color-mix(in oklch, var(--foreground) 40%, var(--background));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Шрифты дизайн-системы (Geist / Geist Mono) — только пока смонтирован макет. */
|
||||||
|
html[data-preview-density] {
|
||||||
|
--font-sans: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-mono: 'Geist Mono Variable', ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-preview-density],
|
||||||
|
html[data-preview-density] body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
@@ -79,7 +79,7 @@ export function AppLinksPage() {
|
|||||||
|
|
||||||
const columns = useMemo<GridColumn<LinkRow>[]>(
|
const columns = useMemo<GridColumn<LinkRow>[]>(
|
||||||
() => [
|
() => [
|
||||||
{ id: 'name', title: 'Приложение', size: 260, cell: (r) => <TwoLineCell title={r.name} sub={r.id} /> },
|
{ id: 'name', title: 'Приложение', size: 260, cell: (r) => <TwoLineCell dot={r.enabled ? 'ok' : 'mu'} title={r.name} sub={r.id} /> },
|
||||||
{ id: 'menu', title: 'В меню', size: 150, cell: (r) => <StatusBadge tone={r.enabled ? 'ok' : 'mu'}>{r.enabled ? 'В меню' : 'Скрыто'}</StatusBadge> },
|
{ id: 'menu', title: 'В меню', size: 150, cell: (r) => <StatusBadge tone={r.enabled ? 'ok' : 'mu'}>{r.enabled ? 'В меню' : 'Скрыто'}</StatusBadge> },
|
||||||
{ id: 'icon', title: 'Иконка', size: 110, cell: (r) => <span className="text-sm">{r.icon}</span> },
|
{ id: 'icon', title: 'Иконка', size: 110, cell: (r) => <span className="text-sm">{r.icon}</span> },
|
||||||
{ id: 'url', title: 'URL', size: 260, cell: (r) => <code className="block truncate font-mono text-[13px]">{r.url}</code> },
|
{ id: 'url', title: 'URL', size: 260, cell: (r) => <code className="block truncate font-mono text-[13px]">{r.url}</code> },
|
||||||
@@ -193,7 +193,6 @@ export function AppLinksPage() {
|
|||||||
<>
|
<>
|
||||||
<Button variant="outline" onClick={() => setEditing(null)}>Отмена</Button>
|
<Button variant="outline" onClick={() => setEditing(null)}>Отмена</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(null)
|
setEditing(null)
|
||||||
toast.success('Приложение сохранено (макет)')
|
toast.success('Приложение сохранено (макет)')
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function whenLabel(iso: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns: GridColumn<AuditLogEntry>[] = [
|
const columns: GridColumn<AuditLogEntry>[] = [
|
||||||
{ id: 'action', title: 'Событие', size: 340, sortValue: (r) => r.action, cell: (r) => <TwoLineCell title={<span className="font-mono text-[13px]">{r.action}</span>} sub={r.summary} /> },
|
{ id: 'action', title: 'Событие', size: 340, sortValue: (r) => r.action, cell: (r) => <TwoLineCell dot={SEVERITY_TONE[r.severity]} title={<span className="font-mono text-[13px]">{r.action}</span>} sub={r.summary} /> },
|
||||||
{ id: 'sev', title: 'Важность', size: 120, sortValue: (r) => r.severity, cell: (r) => <StatusBadge tone={SEVERITY_TONE[r.severity]}>{SEVERITY_LABEL[r.severity]}</StatusBadge> },
|
{ id: 'sev', title: 'Важность', size: 120, sortValue: (r) => r.severity, cell: (r) => <StatusBadge tone={SEVERITY_TONE[r.severity]}>{SEVERITY_LABEL[r.severity]}</StatusBadge> },
|
||||||
{ id: 'src', title: 'Источник', size: 160, sortValue: (r) => r.source_app, cell: (r) => <span className="text-sm">{sourceLabel(r)}</span> },
|
{ id: 'src', title: 'Источник', size: 160, sortValue: (r) => r.source_app, cell: (r) => <span className="text-sm">{sourceLabel(r)}</span> },
|
||||||
{ id: 'who', title: 'Кто', size: 140, cell: (r) => <span className="text-sm">{r.actor_name ?? '—'}</span> },
|
{ id: 'who', title: 'Кто', size: 140, cell: (r) => <span className="text-sm">{r.actor_name ?? '—'}</span> },
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function LoginsPage() {
|
|||||||
|
|
||||||
const loginCols = useMemo<GridColumn<LoginRow>[]>(
|
const loginCols = useMemo<GridColumn<LoginRow>[]>(
|
||||||
() => [
|
() => [
|
||||||
{ id: 'login', title: 'Логин', size: 260, sortValue: (r) => r.login, cell: (r) => <TwoLineCell title={r.login} sub={r.client} /> },
|
{ id: 'login', title: 'Логин', size: 260, sortValue: (r) => r.login, cell: (r) => <TwoLineCell dot={r.tone} title={r.login} sub={r.client} /> },
|
||||||
{ id: 'status', title: 'Результат', size: 120, cell: (r) => <StatusBadge tone={r.tone}>{r.status}</StatusBadge> },
|
{ id: 'status', title: 'Результат', size: 120, cell: (r) => <StatusBadge tone={r.tone}>{r.status}</StatusBadge> },
|
||||||
{ id: 'time', title: 'Время', size: 130, cell: (r) => <span className="text-sm whitespace-nowrap">{r.time}</span> },
|
{ id: 'time', title: 'Время', size: 130, cell: (r) => <span className="text-sm whitespace-nowrap">{r.time}</span> },
|
||||||
{ id: 'ip', title: 'IP', size: 150, sortValue: (r) => r.ip, cell: (r) => <span className="font-mono text-[13px]">{r.ip}</span> },
|
{ id: 'ip', title: 'IP', size: 150, sortValue: (r) => r.ip, cell: (r) => <span className="font-mono text-[13px]">{r.ip}</span> },
|
||||||
@@ -32,7 +32,7 @@ export function LoginsPage() {
|
|||||||
|
|
||||||
const sessionCols = useMemo<GridColumn<SessionRow>[]>(
|
const sessionCols = useMemo<GridColumn<SessionRow>[]>(
|
||||||
() => [
|
() => [
|
||||||
{ id: 'user', title: 'Пользователь', size: 240, sortValue: (r) => r.user, cell: (r) => <TwoLineCell title={r.user} sub={r.note || undefined} /> },
|
{ id: 'user', title: 'Пользователь', size: 240, sortValue: (r) => r.user, cell: (r) => <TwoLineCell dot={r.tone} title={r.user} sub={r.note || undefined} /> },
|
||||||
{ id: 'status', title: 'Статус', size: 110, cell: (r) => <StatusBadge tone={r.tone}>{r.status}</StatusBadge> },
|
{ id: 'status', title: 'Статус', size: 110, cell: (r) => <StatusBadge tone={r.tone}>{r.status}</StatusBadge> },
|
||||||
{ id: 'ip', title: 'IP · клиент', size: 260, cell: (r) => <span className="text-sm">{r.ipClient}</span> },
|
{ id: 'ip', title: 'IP · клиент', size: 260, cell: (r) => <span className="text-sm">{r.ipClient}</span> },
|
||||||
{ id: 'created', title: 'Создана', size: 140, cell: (r) => <span className="text-sm whitespace-nowrap">{r.created}</span> },
|
{ id: 'created', title: 'Создана', size: 140, cell: (r) => <span className="text-sm whitespace-nowrap">{r.created}</span> },
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function OidcPage() {
|
|||||||
|
|
||||||
const columns = useMemo<GridColumn<OidcClientRow>[]>(
|
const columns = useMemo<GridColumn<OidcClientRow>[]>(
|
||||||
() => [
|
() => [
|
||||||
{ id: 'name', title: 'Клиент', size: 260, sortValue: (r) => r.name, cell: (r) => <TwoLineCell title={r.name} sub={r.hint} /> },
|
{ id: 'name', title: 'Клиент', size: 260, sortValue: (r) => r.name, cell: (r) => <TwoLineCell dot={tone(r)} title={r.name} sub={r.hint} /> },
|
||||||
{ id: 'status', title: 'Статус', size: 150, cell: (r) => <StatusBadge tone={tone(r)}>{statusLabel(r)}</StatusBadge> },
|
{ id: 'status', title: 'Статус', size: 150, cell: (r) => <StatusBadge tone={tone(r)}>{statusLabel(r)}</StatusBadge> },
|
||||||
{ id: 'cid', title: 'client_id', size: 150, cell: (r) => <code className="font-mono text-[13px]">{r.clientId}</code> },
|
{ id: 'cid', title: 'client_id', size: 150, cell: (r) => <code className="font-mono text-[13px]">{r.clientId}</code> },
|
||||||
{ id: 'redirect', title: 'Redirect URI', size: 320, cell: (r) => <code className="block truncate font-mono text-[13px]">{r.redirect}</code> },
|
{ id: 'redirect', title: 'Redirect URI', size: 320, cell: (r) => <code className="block truncate font-mono text-[13px]">{r.redirect}</code> },
|
||||||
@@ -159,7 +159,6 @@ export function OidcPage() {
|
|||||||
<>
|
<>
|
||||||
<Button variant="outline" onClick={() => setEditing(null)}>Закрыть</Button>
|
<Button variant="outline" onClick={() => setEditing(null)}>Закрыть</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditing(null)
|
setEditing(null)
|
||||||
toast.success('Клиент сохранён (макет)')
|
toast.success('Клиент сохранён (макет)')
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function SecurityPage() {
|
|||||||
|
|
||||||
const columns = useMemo<GridColumn<PasskeyRow>[]>(
|
const columns = useMemo<GridColumn<PasskeyRow>[]>(
|
||||||
() => [
|
() => [
|
||||||
{ id: 'name', title: 'Passkey', size: 280, sortValue: (r) => r.name, cell: (r) => <TwoLineCell title={r.name} sub={r.note} /> },
|
{ id: 'name', title: 'Passkey', size: 280, sortValue: (r) => r.name, cell: (r) => <TwoLineCell dot={r.tone} title={r.name} sub={r.note} /> },
|
||||||
{ id: 'kind', title: 'Тип', size: 160, cell: (r) => <span className="text-sm">{r.kind}</span> },
|
{ id: 'kind', title: 'Тип', size: 160, cell: (r) => <span className="text-sm">{r.kind}</span> },
|
||||||
{ id: 'added', title: 'Добавлен', size: 120, cell: (r) => <span className="text-sm tabular-nums">{r.added}</span> },
|
{ id: 'added', title: 'Добавлен', size: 120, cell: (r) => <span className="text-sm tabular-nums">{r.added}</span> },
|
||||||
{ id: 'last', title: 'Последний вход', size: 160, cell: (r) => <span className="text-sm">{r.lastUsed}</span> },
|
{ id: 'last', title: 'Последний вход', size: 160, cell: (r) => <span className="text-sm">{r.lastUsed}</span> },
|
||||||
@@ -189,7 +189,6 @@ export function SecurityPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={() => setSelected(null)}>Закрыть</Button>
|
<Button variant="outline" onClick={() => setSelected(null)}>Закрыть</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setKeys((k) => k.map((x) => (x.id === selected?.id ? { ...x, name: renameValue } : x)))
|
setKeys((k) => k.map((x) => (x.id === selected?.id ? { ...x, name: renameValue } : x)))
|
||||||
setSelected(null)
|
setSelected(null)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
RowsIcon,
|
RowsIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
|
StarIcon,
|
||||||
ShieldIcon,
|
ShieldIcon,
|
||||||
SunIcon,
|
SunIcon,
|
||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
@@ -50,6 +51,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@authportal/ui/components/dropdown-menu'
|
} from '@authportal/ui/components/dropdown-menu'
|
||||||
import { Kbd } from '@authportal/ui/components/kbd'
|
import { Kbd } from '@authportal/ui/components/kbd'
|
||||||
|
import { Switch } from '@authportal/ui/components/switch'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@authportal/ui/components/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@authportal/ui/components/popover'
|
||||||
import { Separator } from '@authportal/ui/components/separator'
|
import { Separator } from '@authportal/ui/components/separator'
|
||||||
import {
|
import {
|
||||||
@@ -61,6 +63,7 @@ import {
|
|||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
|
SidebarMenuAction,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarSeparator,
|
SidebarSeparator,
|
||||||
@@ -68,7 +71,7 @@ import {
|
|||||||
useSidebar,
|
useSidebar,
|
||||||
} from '@authportal/ui/components/sidebar'
|
} from '@authportal/ui/components/sidebar'
|
||||||
import { cn } from '@authportal/ui/lib/utils'
|
import { cn } from '@authportal/ui/lib/utils'
|
||||||
import { APP_SHORT, MOCK_USERS, MONITOR_ITEMS, NAV_GROUPS, NAV_ITEMS } from './mock'
|
import { APP_SHORT, MOCK_USERS, MONITOR_ITEMS, NAV_GROUPS, NAV_ITEMS, type NavItem } from './mock'
|
||||||
import { usePreviewStore } from './preview-store'
|
import { usePreviewStore } from './preview-store'
|
||||||
|
|
||||||
const APP_ICON: Record<AppId, typeof CloudIcon> = {
|
const APP_ICON: Record<AppId, typeof CloudIcon> = {
|
||||||
@@ -297,51 +300,78 @@ function PreviewNavUser() {
|
|||||||
|
|
||||||
const DOT_CLASS = { er: 'bg-destructive', wn: 'bg-warning' } as const
|
const DOT_CLASS = { er: 'bg-destructive', wn: 'bg-warning' } as const
|
||||||
|
|
||||||
|
function NavEntry({ item, pathname, pinned, onTogglePin }: { item: NavItem; pathname: string; pinned: boolean; onTogglePin: () => void }) {
|
||||||
|
const Icon = item.icon
|
||||||
|
const active = pathname === item.to || pathname.startsWith(`${item.to}/`)
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
tooltip={item.problem ? `${item.label} · ${item.problem.label}` : item.label}
|
||||||
|
isActive={active}
|
||||||
|
render={<Link to={item.to} />}
|
||||||
|
>
|
||||||
|
<span className="relative flex shrink-0">
|
||||||
|
<Icon className="size-4" />
|
||||||
|
{item.problem ? (
|
||||||
|
<span
|
||||||
|
role="img"
|
||||||
|
aria-label={item.problem.label}
|
||||||
|
className={cn('ring-sidebar absolute -top-1 -right-1 size-2 rounded-full ring-2', DOT_CLASS[item.problem.tone])}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
<SidebarMenuAction
|
||||||
|
showOnHover={!pinned}
|
||||||
|
aria-label={pinned ? `Открепить ${item.label}` : `Закрепить ${item.label}`}
|
||||||
|
aria-pressed={pinned}
|
||||||
|
onClick={onTogglePin}
|
||||||
|
>
|
||||||
|
<StarIcon className={cn(pinned && 'fill-current')} aria-hidden />
|
||||||
|
</SidebarMenuAction>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function PreviewSidebar() {
|
export function PreviewSidebar() {
|
||||||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||||
|
const { pins, togglePin } = usePreviewStore()
|
||||||
|
const pinned = NAV_ITEMS.filter((i) => pins.includes(i.id))
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<PreviewAppSwitcher />
|
<PreviewAppSwitcher />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
|
{pinned.length > 0 ? (
|
||||||
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||||
|
<SidebarGroupLabel>Закреплённые</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{pinned.map((item) => (
|
||||||
|
<NavEntry key={item.id} item={item} pathname={pathname} pinned onTogglePin={() => togglePin(item.id)} />
|
||||||
|
))}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
) : null}
|
||||||
{NAV_GROUPS.map((group, gi) => (
|
{NAV_GROUPS.map((group, gi) => (
|
||||||
<div key={group} className="flex flex-col">
|
<div key={group} className="flex flex-col">
|
||||||
{gi > 0 ? (
|
{gi > 0 ? <SidebarSeparator className="mx-2 my-1 hidden group-data-[collapsible=icon]:block" /> : null}
|
||||||
<SidebarSeparator className="mx-2 my-1 hidden group-data-[collapsible=icon]:block" />
|
|
||||||
) : null}
|
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>{group}</SidebarGroupLabel>
|
<SidebarGroupLabel>{group}</SidebarGroupLabel>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{NAV_ITEMS.filter((i) => i.group === group).map((item) => {
|
{NAV_ITEMS.filter((i) => i.group === group).map((item) => (
|
||||||
const Icon = item.icon
|
<NavEntry
|
||||||
const active = pathname === item.to || pathname.startsWith(`${item.to}/`)
|
key={item.id}
|
||||||
return (
|
item={item}
|
||||||
<SidebarMenuItem key={item.id}>
|
pathname={pathname}
|
||||||
<SidebarMenuButton
|
pinned={pins.includes(item.id)}
|
||||||
tooltip={item.problem ? `${item.label} · ${item.problem.label}` : item.label}
|
onTogglePin={() => togglePin(item.id)}
|
||||||
isActive={active}
|
/>
|
||||||
render={<Link to={item.to} />}
|
))}
|
||||||
>
|
|
||||||
<span className="relative flex shrink-0">
|
|
||||||
<Icon className="size-4" />
|
|
||||||
{item.problem ? (
|
|
||||||
<span
|
|
||||||
role="img"
|
|
||||||
aria-label={item.problem.label}
|
|
||||||
className={cn(
|
|
||||||
'ring-sidebar absolute -top-1 -right-1 size-2 rounded-full ring-2',
|
|
||||||
DOT_CLASS[item.problem.tone],
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
@@ -407,6 +437,7 @@ function MonitorPopover() {
|
|||||||
export function PreviewHeader() {
|
export function PreviewHeader() {
|
||||||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||||
const { setPaletteOpen } = usePreviewStore()
|
const { setPaletteOpen } = usePreviewStore()
|
||||||
|
const [auto, setAuto] = useState(true)
|
||||||
const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`))
|
const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -425,15 +456,21 @@ export function PreviewHeader() {
|
|||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<div className="ml-auto flex shrink-0 items-center gap-2">
|
<div className="ml-auto flex shrink-0 items-center gap-2">
|
||||||
|
<span role="status" className="text-muted-foreground hidden items-center gap-1.5 text-xs @3xl/header:flex">
|
||||||
|
<span className="bg-success size-1.5 rounded-full" aria-hidden />
|
||||||
|
Обновлено 14:02:11 · Live
|
||||||
|
<Switch size="sm" checked={auto} onCheckedChange={setAuto} aria-label="Автообновление" />
|
||||||
|
<span>авто</span>
|
||||||
|
</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="icon-sm"
|
||||||
aria-label="Командная палитра"
|
aria-label="Командная палитра (⌘K)"
|
||||||
|
className="md:hidden"
|
||||||
onClick={() => setPaletteOpen(true)}
|
onClick={() => setPaletteOpen(true)}
|
||||||
>
|
>
|
||||||
<SearchIcon aria-hidden />
|
<SearchIcon aria-hidden />
|
||||||
<Kbd className="hidden @xl/header:inline-flex">⌘K</Kbd>
|
|
||||||
</Button>
|
</Button>
|
||||||
<MonitorPopover />
|
<MonitorPopover />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,25 @@ export function PreviewProvider({ children }: { children: ReactNode }) {
|
|||||||
const [paletteOpen, setPaletteOpen] = useState(false)
|
const [paletteOpen, setPaletteOpen] = useState(false)
|
||||||
const [density, setDensity] = useState<Density>('comfortable')
|
const [density, setDensity] = useState<Density>('comfortable')
|
||||||
const [contrast, setContrast] = useState<Contrast>('standard')
|
const [contrast, setContrast] = useState<Contrast>('standard')
|
||||||
|
const [pins, setPins] = useState<string[]>(() => {
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem('design-preview:pins')
|
||||||
|
return raw ? (JSON.parse(raw) as string[]) : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const togglePin = useCallback((id: string) => {
|
||||||
|
setPins((prev) => {
|
||||||
|
const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem('design-preview:pins', JSON.stringify(next))
|
||||||
|
} catch {
|
||||||
|
/* без сохранения закреплённых */
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
const seq = useRef(0)
|
const seq = useRef(0)
|
||||||
const pendingRef = useRef(pending)
|
const pendingRef = useRef(pending)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,7 +79,7 @@ export function PreviewProvider({ children }: { children: ReactNode }) {
|
|||||||
const count = pendingRef.current.length
|
const count = pendingRef.current.length
|
||||||
setPending([])
|
setPending([])
|
||||||
toast.success(`Сохранено изменений: ${count}`, {
|
toast.success(`Сохранено изменений: ${count}`, {
|
||||||
description: 'Макет: записи в журнал не отправляются.',
|
description: 'Записано в портал и журнал «Изменения» (макет: ничего не отправляется).',
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -72,12 +91,14 @@ export function PreviewProvider({ children }: { children: ReactNode }) {
|
|||||||
applyAll,
|
applyAll,
|
||||||
paletteOpen,
|
paletteOpen,
|
||||||
setPaletteOpen,
|
setPaletteOpen,
|
||||||
|
pins,
|
||||||
|
togglePin,
|
||||||
density,
|
density,
|
||||||
setDensity,
|
setDensity,
|
||||||
contrast,
|
contrast,
|
||||||
setContrast,
|
setContrast,
|
||||||
}),
|
}),
|
||||||
[pending, addChange, discardAll, applyAll, paletteOpen, density, contrast],
|
[pending, addChange, discardAll, applyAll, paletteOpen, pins, togglePin, density, contrast],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,7 +9,16 @@ import './design-preview.css'
|
|||||||
/** Оболочка макета: сайдбар, шапка, палитра и панель «Не сохранено». Монтируется после гидрации. */
|
/** Оболочка макета: сайдбар, шапка, палитра и панель «Не сохранено». Монтируется после гидрации. */
|
||||||
export function PreviewShell({ children }: { children: ReactNode }) {
|
export function PreviewShell({ children }: { children: ReactNode }) {
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
useEffect(() => setMounted(true), [])
|
const [open, setOpen] = useState(true)
|
||||||
|
useEffect(() => {
|
||||||
|
// До 1100px меню сворачивается в колонку иконок (на мобильных его заменяет Sheet).
|
||||||
|
const mq = window.matchMedia('(min-width: 1100px)')
|
||||||
|
const sync = () => setOpen(mq.matches)
|
||||||
|
sync()
|
||||||
|
mq.addEventListener('change', sync)
|
||||||
|
setMounted(true)
|
||||||
|
return () => mq.removeEventListener('change', sync)
|
||||||
|
}, [])
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
@@ -22,7 +31,7 @@ export function PreviewShell({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delay={0}>
|
<TooltipProvider delay={0}>
|
||||||
<SidebarProvider style={{ '--sidebar-width': '240px' } as CSSProperties}>
|
<SidebarProvider open={open} onOpenChange={setOpen} style={{ '--sidebar-width': '240px' } as CSSProperties}>
|
||||||
<a
|
<a
|
||||||
href="#main-content"
|
href="#main-content"
|
||||||
className="bg-background text-foreground ring-ring sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:rounded-md focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:ring-2"
|
className="bg-background text-foreground ring-ring sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:rounded-md focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:ring-2"
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export type PreviewStore = {
|
|||||||
applyAll: () => void
|
applyAll: () => void
|
||||||
paletteOpen: boolean
|
paletteOpen: boolean
|
||||||
setPaletteOpen: (open: boolean) => void
|
setPaletteOpen: (open: boolean) => void
|
||||||
|
pins: string[]
|
||||||
|
togglePin: (id: string) => void
|
||||||
density: Density
|
density: Density
|
||||||
setDensity: (v: Density) => void
|
setDensity: (v: Density) => void
|
||||||
contrast: Contrast
|
contrast: Contrast
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import { Separator } from '@authportal/ui/components/separator'
|
|||||||
import { InputGroup, InputGroupAddon, InputGroupInput } from '@authportal/ui/components/input-group'
|
import { InputGroup, InputGroupAddon, InputGroupInput } from '@authportal/ui/components/input-group'
|
||||||
import { cn } from '@authportal/ui/lib/utils'
|
import { cn } from '@authportal/ui/lib/utils'
|
||||||
import { Segmented } from './bits'
|
import { Segmented } from './bits'
|
||||||
|
import type { Tone } from './mock'
|
||||||
|
import { TONE_DOT } from './tone'
|
||||||
|
|
||||||
export type GridColumn<T> = {
|
export type GridColumn<T> = {
|
||||||
id: string
|
id: string
|
||||||
@@ -84,7 +86,23 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
emptyMessage?: string
|
emptyMessage?: string
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const [view, setView] = useState(0)
|
const storageKey = `design-preview:view:${title}`
|
||||||
|
const [view, setViewState] = useState(() => {
|
||||||
|
try {
|
||||||
|
const v = Number(window.localStorage.getItem(storageKey))
|
||||||
|
return Number.isInteger(v) && v >= 0 && v < (views?.length ?? 0) ? v : 0
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const setView = (i: number) => {
|
||||||
|
setViewState(i)
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(storageKey, String(i))
|
||||||
|
} catch {
|
||||||
|
/* без сохранения вида */
|
||||||
|
}
|
||||||
|
}
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [sorting, setSorting] = useState<SortingState>([])
|
const [sorting, setSorting] = useState<SortingState>([])
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
@@ -170,7 +188,7 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
{views ? (
|
{views ? (
|
||||||
<Segmented
|
<Segmented
|
||||||
label="Вид"
|
label="Вид"
|
||||||
options={views.map((v) => v.label)}
|
options={views.map((v) => `${v.label} · ${rows.filter(v.match).length}`)}
|
||||||
value={view}
|
value={view}
|
||||||
onChange={(i) => {
|
onChange={(i) => {
|
||||||
setView(i)
|
setView(i)
|
||||||
@@ -233,11 +251,14 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Двухстрочная ячейка: заголовок + подпись. */
|
/** Двухстрочная ячейка: заголовок + подпись. */
|
||||||
export function TwoLineCell({ title, sub }: { title: ReactNode; sub?: ReactNode }) {
|
export function TwoLineCell({ title, sub, dot }: { title: ReactNode; sub?: ReactNode; dot?: Tone }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 flex-col">
|
<div className="flex min-w-0 items-start gap-2">
|
||||||
<span className="truncate text-sm font-medium">{title}</span>
|
{dot ? <span aria-hidden className={cn('mt-1.5 size-2 shrink-0 rounded-full', TONE_DOT[dot])} /> : null}
|
||||||
{sub ? <span className="text-muted-foreground truncate text-xs">{sub}</span> : null}
|
<div className="flex min-w-0 flex-col">
|
||||||
|
<span className="truncate text-sm font-medium">{title}</span>
|
||||||
|
{sub ? <span className="text-muted-foreground truncate font-mono text-[11px]">{sub}</span> : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Generated
+73
-8
@@ -129,7 +129,7 @@ importers:
|
|||||||
version: 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
|
version: 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.1.10(@types/node@22.20.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1))
|
version: 4.1.10(@types/node@22.20.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))
|
||||||
|
|
||||||
apps/web:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -142,6 +142,12 @@ importers:
|
|||||||
'@base-ui/react':
|
'@base-ui/react':
|
||||||
specifier: ^1.5.0
|
specifier: ^1.5.0
|
||||||
version: 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
'@fontsource-variable/geist':
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.3.0
|
||||||
|
'@fontsource-variable/geist-mono':
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.3.0
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.4.0
|
specifier: ^5.4.0
|
||||||
version: 5.4.0(react-hook-form@7.82.0(react@19.2.7))
|
version: 5.4.0(react-hook-form@7.82.0(react@19.2.7))
|
||||||
@@ -303,7 +309,7 @@ importers:
|
|||||||
version: 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
|
version: 8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))
|
version: 4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1))
|
||||||
|
|
||||||
packages/ui:
|
packages/ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1261,6 +1267,12 @@ packages:
|
|||||||
'@floating-ui/utils@0.2.12':
|
'@floating-ui/utils@0.2.12':
|
||||||
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
|
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
|
||||||
|
|
||||||
|
'@fontsource-variable/geist-mono@5.3.0':
|
||||||
|
resolution: {integrity: sha512-vBbuwDEo9AkrqADMXOrlAR3DFcJi4/JxeuU43FoiQERnNwsfXNnvxvReZG02cQKmyk4DZkZdBZX3oTDvy2zBAw==}
|
||||||
|
|
||||||
|
'@fontsource-variable/geist@5.3.0':
|
||||||
|
resolution: {integrity: sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==}
|
||||||
|
|
||||||
'@hexagon/base64@1.1.28':
|
'@hexagon/base64@1.1.28':
|
||||||
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
||||||
|
|
||||||
@@ -5705,6 +5717,10 @@ snapshots:
|
|||||||
|
|
||||||
'@floating-ui/utils@0.2.12': {}
|
'@floating-ui/utils@0.2.12': {}
|
||||||
|
|
||||||
|
'@fontsource-variable/geist-mono@5.3.0': {}
|
||||||
|
|
||||||
|
'@fontsource-variable/geist@5.3.0': {}
|
||||||
|
|
||||||
'@hexagon/base64@1.1.28': {}
|
'@hexagon/base64@1.1.28': {}
|
||||||
|
|
||||||
'@hookform/resolvers@5.4.0(react-hook-form@7.82.0(react@19.2.7))':
|
'@hookform/resolvers@5.4.0(react-hook-form@7.82.0(react@19.2.7))':
|
||||||
@@ -6666,13 +6682,21 @@ snapshots:
|
|||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
tinyrainbow: 3.1.0
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
'@vitest/mocker@4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1))':
|
'@vitest/mocker@4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.1.10
|
'@vitest/spy': 4.1.10
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)
|
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1))':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 4.1.10
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)
|
||||||
|
|
||||||
'@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))':
|
'@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9121,7 +9145,7 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite@8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1):
|
vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
picomatch: 4.0.5
|
picomatch: 4.0.5
|
||||||
@@ -9130,6 +9154,20 @@ snapshots:
|
|||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.20.1
|
'@types/node': 22.20.1
|
||||||
|
esbuild: 0.28.1
|
||||||
|
fsevents: 2.3.3
|
||||||
|
jiti: 2.7.0
|
||||||
|
tsx: 4.23.1
|
||||||
|
|
||||||
|
vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1):
|
||||||
|
dependencies:
|
||||||
|
lightningcss: 1.32.0
|
||||||
|
picomatch: 4.0.5
|
||||||
|
postcss: 8.5.19
|
||||||
|
rolldown: 1.1.5
|
||||||
|
tinyglobby: 0.2.17
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 24.13.3
|
||||||
esbuild: 0.27.7
|
esbuild: 0.27.7
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
@@ -9149,10 +9187,10 @@ snapshots:
|
|||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
tsx: 4.23.1
|
tsx: 4.23.1
|
||||||
|
|
||||||
vitest@4.1.10(@types/node@22.20.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)):
|
vitest@4.1.10(@types/node@22.20.1)(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.10
|
'@vitest/expect': 4.1.10
|
||||||
'@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1))
|
'@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1))
|
||||||
'@vitest/pretty-format': 4.1.10
|
'@vitest/pretty-format': 4.1.10
|
||||||
'@vitest/runner': 4.1.10
|
'@vitest/runner': 4.1.10
|
||||||
'@vitest/snapshot': 4.1.10
|
'@vitest/snapshot': 4.1.10
|
||||||
@@ -9169,13 +9207,40 @@ snapshots:
|
|||||||
tinyexec: 1.2.4
|
tinyexec: 1.2.4
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
tinyrainbow: 3.1.0
|
tinyrainbow: 3.1.0
|
||||||
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)
|
vite: 8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.20.1
|
'@types/node': 22.20.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
|
||||||
|
vitest@4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)):
|
||||||
|
dependencies:
|
||||||
|
'@vitest/expect': 4.1.10
|
||||||
|
'@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1))
|
||||||
|
'@vitest/pretty-format': 4.1.10
|
||||||
|
'@vitest/runner': 4.1.10
|
||||||
|
'@vitest/snapshot': 4.1.10
|
||||||
|
'@vitest/spy': 4.1.10
|
||||||
|
'@vitest/utils': 4.1.10
|
||||||
|
es-module-lexer: 2.3.1
|
||||||
|
expect-type: 1.4.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
obug: 2.1.4
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.5
|
||||||
|
std-env: 4.2.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 1.2.4
|
||||||
|
tinyglobby: 0.2.17
|
||||||
|
tinyrainbow: 3.1.0
|
||||||
|
vite: 8.1.5(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.23.1)
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 24.13.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- msw
|
||||||
|
|
||||||
vitest@4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)):
|
vitest@4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.10
|
'@vitest/expect': 4.1.10
|
||||||
|
|||||||
Reference in new issue
Block a user