feat(design-preview): привести макет к дизайн-системе shnt Ops

Шрифты Geist и Geist Mono (только пока смонтирован макет), плашки статуса с глифом
и точкой в первой колонке, счётчики в видах таблиц с запоминанием вида,
закреплённые пункты меню, автосворачивание меню до 1100px, индикатор свежести
в шапке, вызов палитры кнопкой только на узких экранах, главная кнопка в Sheet
и предупреждение «Закрыть без сохранения».

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-17 22:29:47 +07:00
1 parent bd69b27e5c
commit 67a0510eeb
15 files changed
+292 -66

No files matched your search

+2
View File
@@ -16,6 +16,8 @@
"@authportal/shared": "workspace:*",
"@authportal/ui": "workspace:*",
"@base-ui/react": "^1.5.0",
"@fontsource-variable/geist": "^5.3.0",
"@fontsource-variable/geist-mono": "^5.3.0",
"@hookform/resolvers": "^5.4.0",
"@simplewebauthn/browser": "^13.3.0",
"@tailwindcss/vite": "^4.3.1",
@@ -181,7 +181,6 @@ export function AccessDialog({
Отмена
</Button>
<Button
variant="outline"
disabled={lines.length === 0}
onClick={() => {
onSave(next)
+62 -3
View File
@@ -35,9 +35,14 @@ import {
import type { Tone } from './mock'
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 }) {
const glyph = TONE_GLYPH[tone]
return (
<Badge variant={TONE_BADGE[tone]} size="sm" className="whitespace-nowrap">
{glyph ? <span aria-hidden className="text-[0.55rem]">{glyph}</span> : null}
{children}
</Badge>
)
@@ -122,8 +127,21 @@ export function PreviewSheet({
children: 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 (
<Sheet open={open} onOpenChange={onOpenChange}>
<>
<Sheet open={open} onOpenChange={requestChange}>
<SheetContent
side="right"
showCloseButton={false}
@@ -150,12 +168,53 @@ export function PreviewSheet({
{description}
</SheetDescription>
</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 ? (
<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}
</SheetContent>
</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 и снимает при выходе. */
html[data-preview-density='compact'] [data-slot='data-grid-table'] :is(th, td) {
padding-block: 0.25rem;
@@ -12,3 +15,14 @@ html[data-preview-contrast='high'] {
--border: color-mix(in oklch, var(--foreground) 32%, 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>[]>(
() => [
{ 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: '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> },
@@ -193,7 +193,6 @@ export function AppLinksPage() {
<>
<Button variant="outline" onClick={() => setEditing(null)}>Отмена</Button>
<Button
variant="outline"
onClick={() => {
setEditing(null)
toast.success('Приложение сохранено (макет)')
@@ -24,7 +24,7 @@ function whenLabel(iso: string) {
}
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: '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> },
@@ -20,7 +20,7 @@ export function LoginsPage() {
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: '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> },
@@ -32,7 +32,7 @@ export function LoginsPage() {
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: '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> },
@@ -45,7 +45,7 @@ export function OidcPage() {
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: '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> },
@@ -159,7 +159,6 @@ export function OidcPage() {
<>
<Button variant="outline" onClick={() => setEditing(null)}>Закрыть</Button>
<Button
variant="outline"
onClick={() => {
setEditing(null)
toast.success('Клиент сохранён (макет)')
@@ -23,7 +23,7 @@ export function SecurityPage() {
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: '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> },
@@ -189,7 +189,6 @@ export function SecurityPage() {
</Button>
<Button variant="outline" onClick={() => setSelected(null)}>Закрыть</Button>
<Button
variant="outline"
onClick={() => {
setKeys((k) => k.map((x) => (x.id === selected?.id ? { ...x, name: renameValue } : x)))
setSelected(null)
+72 -35
View File
@@ -17,6 +17,7 @@ import {
RowsIcon,
SearchIcon,
ServerIcon,
StarIcon,
ShieldIcon,
SunIcon,
UserPlusIcon,
@@ -50,6 +51,7 @@ import {
DropdownMenuTrigger,
} from '@authportal/ui/components/dropdown-menu'
import { Kbd } from '@authportal/ui/components/kbd'
import { Switch } from '@authportal/ui/components/switch'
import { Popover, PopoverContent, PopoverTrigger } from '@authportal/ui/components/popover'
import { Separator } from '@authportal/ui/components/separator'
import {
@@ -61,6 +63,7 @@ import {
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarSeparator,
@@ -68,7 +71,7 @@ import {
useSidebar,
} from '@authportal/ui/components/sidebar'
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'
const APP_ICON: Record<AppId, typeof CloudIcon> = {
@@ -297,51 +300,78 @@ function PreviewNavUser() {
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() {
const pathname = useRouterState({ select: (s) => s.location.pathname })
const { pins, togglePin } = usePreviewStore()
const pinned = NAV_ITEMS.filter((i) => pins.includes(i.id))
return (
<Sidebar collapsible="icon">
<SidebarHeader>
<PreviewAppSwitcher />
</SidebarHeader>
<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) => (
<div key={group} className="flex flex-col">
{gi > 0 ? (
<SidebarSeparator className="mx-2 my-1 hidden group-data-[collapsible=icon]:block" />
) : null}
{gi > 0 ? <SidebarSeparator className="mx-2 my-1 hidden group-data-[collapsible=icon]:block" /> : null}
<SidebarGroup>
<SidebarGroupLabel>{group}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{NAV_ITEMS.filter((i) => i.group === group).map((item) => {
const Icon = item.icon
const active = pathname === item.to || pathname.startsWith(`${item.to}/`)
return (
<SidebarMenuItem key={item.id}>
<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>
</SidebarMenuItem>
)
})}
{NAV_ITEMS.filter((i) => i.group === group).map((item) => (
<NavEntry
key={item.id}
item={item}
pathname={pathname}
pinned={pins.includes(item.id)}
onTogglePin={() => togglePin(item.id)}
/>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
@@ -407,6 +437,7 @@ function MonitorPopover() {
export function PreviewHeader() {
const pathname = useRouterState({ select: (s) => s.location.pathname })
const { setPaletteOpen } = usePreviewStore()
const [auto, setAuto] = useState(true)
const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`))
return (
@@ -425,15 +456,21 @@ export function PreviewHeader() {
</BreadcrumbList>
</Breadcrumb>
<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
type="button"
variant="outline"
size="sm"
aria-label="Командная палитра"
size="icon-sm"
aria-label="Командная палитра (⌘K)"
className="md:hidden"
onClick={() => setPaletteOpen(true)}
>
<SearchIcon aria-hidden />
<Kbd className="hidden @xl/header:inline-flex">⌘K</Kbd>
</Button>
<MonitorPopover />
</div>
@@ -17,6 +17,25 @@ export function PreviewProvider({ children }: { children: ReactNode }) {
const [paletteOpen, setPaletteOpen] = useState(false)
const [density, setDensity] = useState<Density>('comfortable')
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 pendingRef = useRef(pending)
useEffect(() => {
@@ -60,7 +79,7 @@ export function PreviewProvider({ children }: { children: ReactNode }) {
const count = pendingRef.current.length
setPending([])
toast.success(`Сохранено изменений: ${count}`, {
description: 'Макет: записи в журнал не отправляются.',
description: 'Записано в портал и журнал «Изменения» (макет: ничего не отправляется).',
})
}, [])
@@ -72,12 +91,14 @@ export function PreviewProvider({ children }: { children: ReactNode }) {
applyAll,
paletteOpen,
setPaletteOpen,
pins,
togglePin,
density,
setDensity,
contrast,
setContrast,
}),
[pending, addChange, discardAll, applyAll, paletteOpen, density, contrast],
[pending, addChange, discardAll, applyAll, paletteOpen, pins, togglePin, density, contrast],
)
return (
+11 -2
View File
@@ -9,7 +9,16 @@ import './design-preview.css'
/** Оболочка макета: сайдбар, шапка, палитра и панель «Не сохранено». Монтируется после гидрации. */
export function PreviewShell({ children }: { children: ReactNode }) {
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) {
return (
@@ -22,7 +31,7 @@ export function PreviewShell({ children }: { children: ReactNode }) {
return (
<TooltipProvider delay={0}>
<SidebarProvider style={{ '--sidebar-width': '240px' } as CSSProperties}>
<SidebarProvider open={open} onOpenChange={setOpen} style={{ '--sidebar-width': '240px' } as CSSProperties}>
<a
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"
@@ -22,6 +22,8 @@ export type PreviewStore = {
applyAll: () => void
paletteOpen: boolean
setPaletteOpen: (open: boolean) => void
pins: string[]
togglePin: (id: string) => void
density: Density
setDensity: (v: Density) => void
contrast: Contrast
+27 -6
View File
@@ -31,6 +31,8 @@ import { Separator } from '@authportal/ui/components/separator'
import { InputGroup, InputGroupAddon, InputGroupInput } from '@authportal/ui/components/input-group'
import { cn } from '@authportal/ui/lib/utils'
import { Segmented } from './bits'
import type { Tone } from './mock'
import { TONE_DOT } from './tone'
export type GridColumn<T> = {
id: string
@@ -84,7 +86,23 @@ export function PreviewSectionGrid<T extends object>({
emptyMessage?: 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 [sorting, setSorting] = useState<SortingState>([])
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
@@ -170,7 +188,7 @@ export function PreviewSectionGrid<T extends object>({
{views ? (
<Segmented
label="Вид"
options={views.map((v) => v.label)}
options={views.map((v) => `${v.label} · ${rows.filter(v.match).length}`)}
value={view}
onChange={(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 (
<div className="flex min-w-0 flex-col">
<span className="truncate text-sm font-medium">{title}</span>
{sub ? <span className="text-muted-foreground truncate text-xs">{sub}</span> : null}
<div className="flex min-w-0 items-start gap-2">
{dot ? <span aria-hidden className={cn('mt-1.5 size-2 shrink-0 rounded-full', TONE_DOT[dot])} /> : 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>
)
}
+73 -8
View File
@@ -129,7 +129,7 @@ importers:
version: 8.64.0([email protected]([email protected]))([email protected])
vitest:
specifier: 'catalog:'
version: 4.1.10(@types/[email protected])([email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected]))
version: 4.1.10(@types/[email protected])([email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected]))
apps/web:
dependencies:
@@ -142,6 +142,12 @@ importers:
'@base-ui/react':
specifier: ^1.5.0
version: 1.6.0(@date-fns/[email protected])(@types/[email protected])([email protected])([email protected]([email protected]))([email protected])
'@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':
specifier: ^5.4.0
version: 5.4.0([email protected]([email protected]))
@@ -303,7 +309,7 @@ importers:
version: 8.64.0([email protected]([email protected]))([email protected])
vitest:
specifier: 'catalog:'
version: 4.1.10(@types/[email protected])([email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected]))
version: 4.1.10(@types/[email protected])([email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected]))
packages/ui:
dependencies:
@@ -1261,6 +1267,12 @@ packages:
'@floating-ui/[email protected]':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@fontsource-variable/[email protected]':
resolution: {integrity: sha512-vBbuwDEo9AkrqADMXOrlAR3DFcJi4/JxeuU43FoiQERnNwsfXNnvxvReZG02cQKmyk4DZkZdBZX3oTDvy2zBAw==}
'@fontsource-variable/[email protected]':
resolution: {integrity: sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==}
'@hexagon/[email protected]':
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
@@ -5705,6 +5717,10 @@ snapshots:
'@floating-ui/[email protected]': {}
'@fontsource-variable/[email protected]': {}
'@fontsource-variable/[email protected]': {}
'@hexagon/[email protected]': {}
'@hookform/[email protected]([email protected]([email protected]))':
@@ -6666,13 +6682,21 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/[email protected]([email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected]))':
'@vitest/[email protected]([email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected]))':
dependencies:
'@vitest/spy': 4.1.10
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.1.5(@types/[email protected])([email protected]7.7)([email protected])([email protected])
vite: 8.1.5(@types/[email protected])([email protected]8.1)([email protected])([email protected])
'@vitest/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected]))':
dependencies:
'@vitest/spy': 4.1.10
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.1.5(@types/[email protected])([email protected])([email protected])([email protected])
'@vitest/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected]))':
dependencies:
@@ -9121,7 +9145,7 @@ snapshots:
[email protected]: {}
[email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected]):
[email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected]):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.5
@@ -9130,6 +9154,20 @@ snapshots:
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 22.20.1
esbuild: 0.28.1
fsevents: 2.3.3
jiti: 2.7.0
tsx: 4.23.1
[email protected](@types/[email protected])([email protected])([email protected])([email protected]):
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
fsevents: 2.3.3
jiti: 2.7.0
@@ -9149,10 +9187,10 @@ snapshots:
jiti: 2.7.0
tsx: 4.23.1
[email protected](@types/[email protected])([email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected])):
[email protected](@types/[email protected])([email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected])):
dependencies:
'@vitest/expect': 4.1.10
'@vitest/mocker': 4.1.10([email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected]))
'@vitest/mocker': 4.1.10([email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected]))
'@vitest/pretty-format': 4.1.10
'@vitest/runner': 4.1.10
'@vitest/snapshot': 4.1.10
@@ -9169,13 +9207,40 @@ snapshots:
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.1.5(@types/[email protected])([email protected]7.7)([email protected])([email protected])
vite: 8.1.5(@types/[email protected])([email protected]8.1)([email protected])([email protected])
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.20.1
transitivePeerDependencies:
- msw
[email protected](@types/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])):
dependencies:
'@vitest/expect': 4.1.10
'@vitest/mocker': 4.1.10([email protected](@types/[email protected])([email protected])([email protected])([email protected]))
'@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/[email protected])([email protected])([email protected])([email protected])
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.13.3
transitivePeerDependencies:
- msw
[email protected](@types/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])):
dependencies:
'@vitest/expect': 4.1.10