feat(ui): фаза 5 — Тема 2.0 во всех базовых ui/* (select/textarea/tabs/popover/tooltip/dropdown-menu/checkbox/dialog на рецептах и токенах), data-density/control-variant/button-radius на html, консолидация размеров кнопок (44 места → size=sm), max-h+скролл диалогов

This commit is contained in:
2026-09-08 21:23:25 +03:00
parent 113ed458b6
commit aa8bf6ac33
22 changed files with 126 additions and 74 deletions

View File

@@ -54,14 +54,14 @@ export function DeleteTaskDialog({ open: showDeleteDialog, onOpenChange: setShow
<DialogFooter>
<Button
variant="outline"
className="h-7 px-2.5 text-xs"
size="sm"
onClick={() => setShowDeleteDialog(false)}
>
Отмена
</Button>
<Button
variant="destructive"
className="h-7 px-2.5 text-xs"
size="sm"
onClick={() => deleteTaskMutation.mutate()}
disabled={deleteTaskMutation.isPending}
>

View File

@@ -332,7 +332,6 @@ export function ReminderDialog({ open: showReminderDialog, onOpenChange: setShow
variant="outline"
size="sm"
onClick={() => setShowReminderDialog(false)}
className="h-7 px-2.5 text-xs"
>
Закрыть
</Button>
@@ -381,7 +380,6 @@ export function ReminderDialog({ open: showReminderDialog, onOpenChange: setShow
}
}}
disabled={isReminderSubmitting}
className="h-7 px-2.5 text-xs"
>
<Plus className="w-3 h-3 mr-1" />
{isReminderSubmitting ? 'Создание...' : 'Создать'}

View File

@@ -3,6 +3,7 @@ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
import { checkboxRecipe } from "@/design-system/recipes"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -10,10 +11,7 @@ const Checkbox = React.forwardRef<
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
className={cn(checkboxRecipe, className)}
{...props}
>
<CheckboxPrimitive.Indicator

View File

@@ -51,7 +51,11 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-3 border bg-background p-5 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-md",
// max-h + overflow: длинный контент скроллится внутри диалога и не
// вылезает за viewport; в sheet-режиме на мобильном лимит снимается
// через max-md:max-h-none выше
// Тема 2.0: радиус поверхности — var(--control-radius), fallback sm:rounded-md
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg max-h-[85vh] overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-3 border bg-background p-5 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-[var(--control-radius,calc(var(--radius)_-_2px))]",
dialog.mobile === 'sheet' && MOBILE_SHEET_CLASSES,
className
)}

View File

@@ -3,6 +3,7 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
import { SURFACE_RADIUS } from "@/design-system/recipes"
const DropdownMenu = DropdownMenuPrimitive.Root
@@ -45,7 +46,9 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
// Тема 2.0: радиус поверхности — var(--control-radius), fallback rounded-md
SURFACE_RADIUS,
"z-50 min-w-[8rem] overflow-hidden border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}
@@ -63,7 +66,9 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none focus:outline-none focus-visible:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
// Тема 2.0: радиус поверхности — var(--control-radius), fallback rounded-md
SURFACE_RADIUS,
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden border bg-popover p-1 text-popover-foreground shadow-md outline-none focus:outline-none focus-visible:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}

View File

@@ -2,6 +2,7 @@ import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
import { SURFACE_RADIUS } from "@/design-system/recipes"
const Popover = PopoverPrimitive.Root
@@ -19,7 +20,9 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
// Тема 2.0: радиус поверхности — var(--control-radius), fallback rounded-md
SURFACE_RADIUS,
"z-50 w-72 border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
className
)}
{...props}

View File

@@ -5,6 +5,7 @@ import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
import { selectTriggerRecipe, SURFACE_RADIUS } from "@/design-system/recipes"
const Select = SelectPrimitive.Root
@@ -18,10 +19,8 @@ const SelectTrigger = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-8 w-full items-center justify-between rounded-md border border-border bg-background px-2.5 py-1.5 text-sm ring-offset-background transition-[color,background-color,border-color,box-shadow] duration-150 data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:border-primary disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive",
className
)}
// Тема 2.0: триггер на рецепте дизайн-системы (control-контур + density-высота)
className={cn(selectTriggerRecipe, className)}
{...props}
>
{children}
@@ -75,7 +74,8 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-hidden border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
SURFACE_RADIUS,
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className

View File

@@ -2,6 +2,7 @@ import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
import { tabsRecipe } from "@/design-system/recipes"
const Tabs = TabsPrimitive.Root
@@ -11,10 +12,7 @@ const TabsList = React.forwardRef<
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
className={cn(tabsRecipe.list, className)}
{...props}
/>
))
@@ -26,10 +24,7 @@ const TabsTrigger = React.forwardRef<
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-[color,background-color,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
className={cn(tabsRecipe.trigger, className)}
{...props}
/>
))
@@ -41,10 +36,7 @@ const TabsContent = React.forwardRef<
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
className={cn(tabsRecipe.content, className)}
{...props}
/>
))

View File

@@ -1,6 +1,7 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { textareaRecipe } from "@/design-system/recipes"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
@@ -8,10 +9,7 @@ const Textarea = React.forwardRef<
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[72px] w-full rounded-md border border-border bg-background px-2.5 py-2 text-base md:text-sm ring-offset-background transition-[color,background-color,border-color,box-shadow] duration-150 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-primary disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive",
className
)}
className={cn(textareaRecipe, className)}
ref={ref}
{...props}
/>

View File

@@ -4,6 +4,7 @@ import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
import { SURFACE_RADIUS } from "@/design-system/recipes"
const TooltipProvider = TooltipPrimitive.Provider
@@ -19,7 +20,9 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
// Тема 2.0: радиус поверхности — var(--control-radius), fallback rounded-md
SURFACE_RADIUS,
"z-50 overflow-hidden border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
className
)}
{...props}

View File

@@ -67,6 +67,49 @@ export function inputRecipe({ size = 'md' }: { size?: ControlSize } = {}): strin
return `${INPUT_BASE} ${INPUT_SIZE[size]}`;
}
/* --------------------------------- control ---------------------------------- */
// Общий control-контур Темы 2.0 (бордер/фон/радиус) — переиспользуется
// select-триггером и textarea; fallback повторяет прежний вид
// (rounded-md = calc(var(--radius) - 2px), 1px border-border, bg-background).
const CONTROL_OUTLINE =
'rounded-[var(--control-radius,calc(var(--radius)_-_2px))] border-[length:var(--control-border-width,1px)] border-[color:var(--control-border-color,var(--border))] bg-[color:var(--control-bg,var(--background))]';
// Радиус всплывающих поверхностей (popover, dropdown, select-content, tooltip):
// var(--control-radius), fallback = прежний rounded-md
export const SURFACE_RADIUS =
'rounded-[var(--control-radius,calc(var(--radius)_-_2px))]';
/* ---------------------------------- select ---------------------------------- */
// Триггер селекта — тот же control-контур, что и у инпута; высота — density
// var(--control-height-form) с fallback h-8 (32px), паддинг — var(--control-px)
// с fallback px-2.5 (10px)
export const selectTriggerRecipe = `flex h-[var(--control-height-form,2rem)] w-full items-center justify-between ${CONTROL_OUTLINE} px-[var(--control-px,10px)] py-1.5 text-sm ring-offset-background transition-[color,background-color,border-color,box-shadow] duration-150 data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring focus:border-primary disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive`;
/* --------------------------------- textarea --------------------------------- */
// Тот же control-контур; паддинг — var(--control-px) с fallback px-2.5 (10px)
export const textareaRecipe = `flex min-h-[72px] w-full ${CONTROL_OUTLINE} px-[var(--control-px,10px)] py-2 text-base md:text-sm ring-offset-background transition-[color,background-color,border-color,box-shadow] duration-150 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-primary disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive`;
/* ---------------------------------- tabs ------------------------------------ */
// Радиус списка/триггера — var(--control-radius); fallback: list = rounded-md,
// trigger = rounded-sm (calc(var(--radius) - 4px))
export const tabsRecipe = {
list: 'inline-flex h-10 items-center justify-center rounded-[var(--control-radius,calc(var(--radius)_-_2px))] bg-muted p-1 text-muted-foreground',
trigger:
'inline-flex items-center justify-center whitespace-nowrap rounded-[var(--control-radius,calc(var(--radius)_-_4px))] px-3 py-1.5 text-sm font-medium ring-offset-background transition-[color,background-color,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
content:
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
};
/* --------------------------------- checkbox --------------------------------- */
// Радиус чекбокса — var(--control-radius), fallback = прежний rounded-sm
export const checkboxRecipe =
'peer h-4 w-4 shrink-0 rounded-[var(--control-radius,calc(var(--radius)_-_4px))] border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground';
/* ---------------------------------- badge ----------------------------------- */
export const badgeRecipe = cva(

View File

@@ -295,7 +295,7 @@ export function AutomationsContent() {
<p className="text-xs text-muted-foreground">
JavaScript-скрипты для автоматизации задач. ИИ может создавать и редактировать их через MCP.
</p>
<Button size="sm" className="h-7 px-2.5 text-xs gap-1" onClick={openCreate}>
<Button size="sm" className="gap-1" onClick={openCreate}>
<Plus className="w-3.5 h-3.5" />
Создать
</Button>

View File

@@ -324,7 +324,7 @@ const Forms = () => {
<h1 className="text-sm font-medium text-foreground">Формы</h1>
<p className="text-muted-foreground text-[10px]">Управление формами iistwin</p>
</div>
<Button onClick={() => setCreateModalOpen(true)} className="h-7 px-2.5 text-xs" data-testid="button-create-form">
<Button onClick={() => setCreateModalOpen(true)} size="sm" data-testid="button-create-form">
<Plus className="w-3.5 h-3.5 mr-1.5" />
Создать
</Button>
@@ -350,7 +350,7 @@ const Forms = () => {
/>
</div>
{searchQuery && (
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setSearchQuery('')}>
<Button variant="ghost" size="sm" onClick={() => setSearchQuery('')}>
<X className="w-3 h-3 mr-1" />
Сбросить
</Button>
@@ -434,7 +434,7 @@ const Forms = () => {
/>
</div>
{searchQuery && (
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setSearchQuery('')}>
<Button variant="ghost" size="sm" onClick={() => setSearchQuery('')}>
<X className="w-3 h-3 mr-1" />
Сбросить
</Button>
@@ -516,8 +516,8 @@ const Forms = () => {
</FormItem>
)} />
<DialogFooter>
<Button type="button" variant="outline" className="h-7 px-2.5 text-xs" onClick={() => setCreateModalOpen(false)} data-testid="button-cancel">Отмена</Button>
<Button type="submit" className="h-7 px-2.5 text-xs" disabled={createFormMutation.isPending} data-testid="button-submit">
<Button type="button" variant="outline" size="sm" onClick={() => setCreateModalOpen(false)} data-testid="button-cancel">Отмена</Button>
<Button type="submit" size="sm" disabled={createFormMutation.isPending} data-testid="button-submit">
{createFormMutation.isPending ? 'Создание...' : 'Создать'}
</Button>
</DialogFooter>
@@ -555,8 +555,8 @@ const Forms = () => {
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" className="h-7 px-2.5 text-xs" onClick={() => setDeleteConfirmOpen(false)} data-testid="button-cancel-delete">Отмена</Button>
<Button variant="destructive" className="h-7 px-2.5 text-xs" onClick={confirmDelete} disabled={deleteFormMutation.isPending || loadingTasksCount} data-testid="button-confirm-delete">
<Button variant="outline" size="sm" onClick={() => setDeleteConfirmOpen(false)} data-testid="button-cancel-delete">Отмена</Button>
<Button variant="destructive" size="sm" onClick={confirmDelete} disabled={deleteFormMutation.isPending || loadingTasksCount} data-testid="button-confirm-delete">
{deleteFormMutation.isPending ? 'Удаление...' : 'Удалить'}
</Button>
</DialogFooter>

View File

@@ -254,7 +254,7 @@ export default function GlobalFields() {
Переиспользуемые поля с предзаполненными настройками
</p>
</div>
<Button onClick={openCreateModal} className="h-7 px-2.5 text-xs" data-testid="button-create-field">
<Button onClick={openCreateModal} size="sm" data-testid="button-create-field">
<Plus className="w-3.5 h-3.5 mr-1.5" />
Создать
</Button>

View File

@@ -152,7 +152,7 @@ const InviteRegister = () => {
{error}
</p>
<Link href="/login">
<Button variant="outline" className="h-7 px-2.5 text-xs">
<Button variant="outline" size="sm">
Вернуться на страницу входа
</Button>
</Link>

View File

@@ -65,7 +65,7 @@ export default function NotificationsPage() {
{unreadCount > 0 && (
<Button
variant="outline"
className="h-7 px-2.5 text-xs"
size="sm"
onClick={() => markAllMutation.mutate()}
disabled={markAllMutation.isPending}
data-testid="button-mark-all-read"
@@ -79,7 +79,7 @@ export default function NotificationsPage() {
<div className="flex gap-1 mb-3" role="group" aria-label="Фильтр уведомлений">
<Button
variant={filter === 'all' ? 'default' : 'outline'}
className="h-7 px-2.5 text-xs"
size="sm"
aria-pressed={filter === 'all'}
onClick={() => { setFilter('all'); setVisibleCount(PAGE_SIZE); }}
data-testid="tab-all"
@@ -88,7 +88,7 @@ export default function NotificationsPage() {
</Button>
<Button
variant={filter === 'unread' ? 'default' : 'outline'}
className="h-7 px-2.5 text-xs"
size="sm"
aria-pressed={filter === 'unread'}
onClick={() => { setFilter('unread'); setVisibleCount(PAGE_SIZE); }}
data-testid="tab-unread"

View File

@@ -411,7 +411,7 @@ export default function Tables() {
<h1 className="text-sm font-medium" data-testid="text-directories-title">Справочники</h1>
<p className="text-[10px] text-muted-foreground">Справочники данных для форм</p>
</div>
<Button onClick={() => setCreateDialogOpen(true)} className="h-7 px-2.5 text-xs" data-testid="button-create-directory">
<Button onClick={() => setCreateDialogOpen(true)} size="sm" data-testid="button-create-directory">
<Plus className="w-3.5 h-3.5 mr-1.5" />
Справочник
</Button>
@@ -438,7 +438,7 @@ export default function Tables() {
/>
</div>
{searchQuery && (
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setSearchQuery('')}>
<Button variant="ghost" size="sm" onClick={() => setSearchQuery('')}>
<X className="w-3 h-3 mr-1" />
Сбросить
</Button>
@@ -455,7 +455,7 @@ export default function Tables() {
{searchQuery ? 'Справочники не найдены' : 'Справочники ещё не созданы.'}
</p>
{!searchQuery && (
<Button onClick={() => setCreateDialogOpen(true)} className="h-7 px-2.5 text-xs">
<Button onClick={() => setCreateDialogOpen(true)} size="sm">
<Plus className="w-3.5 h-3.5 mr-1.5" />
Создать справочник
</Button>
@@ -565,14 +565,13 @@ export default function Tables() {
<Button
variant={searchAllFolders ? 'default' : 'outline'}
size="sm"
className="h-7 text-xs"
onClick={() => setSearchAllFolders(v => !v)}
title={searchAllFolders ? 'Искать по всем папкам' : 'Искать только в выбранной папке'}
>
{searchAllFolders ? 'Все папки' : 'Текущая папка'}
</Button>
{selectedFolderId !== null && (
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setSelectedFolderId(null)}>
<Button variant="ghost" size="sm" onClick={() => setSelectedFolderId(null)}>
<X className="w-3 h-3 mr-1" />
Сбросить папку
</Button>
@@ -594,7 +593,7 @@ export default function Tables() {
: 'Справочники не найдены'}
</p>
{!searchQuery && selectedFolderId === null && (
<Button onClick={() => setCreateDialogOpen(true)} className="h-7 px-2.5 text-xs">
<Button onClick={() => setCreateDialogOpen(true)} size="sm">
<Plus className="w-3.5 h-3.5 mr-1.5" />
Создать справочник
</Button>
@@ -770,10 +769,10 @@ export default function Tables() {
</div>
</div>
<DialogFooter>
<Button variant="outline" className="h-7 px-2.5 text-xs" onClick={() => setCreateDialogOpen(false)}>
<Button variant="outline" size="sm" onClick={() => setCreateDialogOpen(false)}>
Отмена
</Button>
<Button className="h-7 px-2.5 text-xs" onClick={handleCreateTable} disabled={createMutation.isPending} data-testid="button-submit-create-table">
<Button size="sm" onClick={handleCreateTable} disabled={createMutation.isPending} data-testid="button-submit-create-table">
{createMutation.isPending ? 'Создание...' : 'Создать'}
</Button>
</DialogFooter>
@@ -789,12 +788,12 @@ export default function Tables() {
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" className="h-7 px-2.5 text-xs" onClick={() => setDeleteDialogOpen(false)}>
<Button variant="outline" size="sm" onClick={() => setDeleteDialogOpen(false)}>
Отмена
</Button>
<Button
variant="destructive"
className="h-7 px-2.5 text-xs"
size="sm"
onClick={() => tableToDelete && deleteMutation.mutate(tableToDelete.id)}
disabled={deleteMutation.isPending}
data-testid="button-confirm-delete"

View File

@@ -398,7 +398,7 @@ const TaskDetail = () => {
<h1 className="text-sm font-medium text-destructive">Ошибка</h1>
<p className="text-muted-foreground text-xs mt-1">Неверный ID формы или задачи</p>
<Link href="/forms">
<Button variant="outline" className="mt-3 h-7 px-2.5 text-xs">
<Button variant="outline" size="sm" className="mt-3">
<ArrowLeft className="w-3.5 h-3.5 mr-1.5" />
Вернуться к формам
</Button>
@@ -496,7 +496,7 @@ const TaskDetail = () => {
<h1 className="text-sm font-medium text-destructive">Форма не найдена</h1>
<p className="text-muted-foreground text-xs mt-1">Запрашиваемая форма не существует</p>
<Link href="/forms">
<Button variant="outline" className="mt-3 h-7 px-2.5 text-xs">
<Button variant="outline" size="sm" className="mt-3">
<ArrowLeft className="w-3.5 h-3.5 mr-1.5" />
Вернуться к формам
</Button>
@@ -513,7 +513,7 @@ const TaskDetail = () => {
<h1 className="text-sm font-medium text-destructive">Задача не найдена</h1>
<p className="text-muted-foreground text-xs mt-1">Запрашиваемая задача не существует</p>
<Link href={`/forms/${formId}/tasks`}>
<Button variant="outline" className="mt-3 h-7 px-2.5 text-xs">
<Button variant="outline" size="sm" className="mt-3">
<ArrowLeft className="w-3.5 h-3.5 mr-1.5" />
Вернуться к задачам
</Button>

View File

@@ -364,7 +364,7 @@ const TaskView = () => {
<h1 className="text-sm font-medium text-destructive">Ошибка</h1>
<p className="text-muted-foreground text-xs mt-1">Неверный ID формы</p>
<Link href="/forms">
<Button variant="outline" className="mt-3 h-7 px-2.5 text-xs">
<Button variant="outline" size="sm" className="mt-3">
<ArrowLeft className="w-3.5 h-3.5 mr-1.5" />
Вернуться к формам
</Button>
@@ -391,7 +391,7 @@ const TaskView = () => {
<h1 className="text-sm font-medium text-destructive">Форма не найдена</h1>
<p className="text-muted-foreground text-xs mt-1">Запрашиваемая форма не существует</p>
<Link href="/forms">
<Button variant="outline" className="mt-3 h-7 px-2.5 text-xs">
<Button variant="outline" size="sm" className="mt-3">
<ArrowLeft className="w-3.5 h-3.5 mr-1.5" />
Вернуться к формам
</Button>
@@ -424,8 +424,8 @@ const TaskView = () => {
{formId === 15 && hasTaskFields && (
<Button
variant="outline"
size="sm"
onClick={() => setQrDialogOpen(true)}
className="h-7 px-2.5 text-xs"
data-testid="button-qr-codes"
>
<Link2 className="w-3.5 h-3.5 mr-1" />
@@ -433,8 +433,8 @@ const TaskView = () => {
</Button>
)}
<Button
size="sm"
onClick={() => setCreateTaskDialogOpen(true)}
className="h-7 px-2.5 text-xs"
data-testid="button-create-task-global"
>
<Plus className="w-3.5 h-3.5 mr-1" />

View File

@@ -69,7 +69,7 @@ const Tasks = () => {
</div>
</div>
<Button
className="h-7 px-2.5 text-xs"
size="sm"
onClick={() => setCreateDialogOpen(true)}
disabled={formsLoading || activeForms.length === 0}
data-testid="button-create-task"
@@ -133,7 +133,7 @@ const Tasks = () => {
<div className="text-center py-4 space-y-2">
<p className="text-xs text-muted-foreground">Нет доступных форм</p>
<Link href="/forms">
<Button variant="outline" className="h-7 px-2.5 text-xs">
<Button variant="outline" size="sm">
Создать форму
<ArrowRight className="w-3 h-3 ml-1" />
</Button>
@@ -182,7 +182,7 @@ function EmptyState({ forms, onCreateClick }: { forms: FormItem[]; onCreateClick
</p>
</div>
<Link href="/forms">
<Button className="h-7 px-2.5 text-xs mt-1">
<Button size="sm" className="mt-1">
<Plus className="w-3.5 h-3.5 mr-1" />
Создать форму
<ArrowRight className="w-3 h-3 ml-1" />
@@ -197,7 +197,7 @@ function EmptyState({ forms, onCreateClick }: { forms: FormItem[]; onCreateClick
Выберите форму и создайте первую задачу
</p>
</div>
<Button className="h-7 px-2.5 text-xs mt-1" onClick={onCreateClick}>
<Button size="sm" className="mt-1" onClick={onCreateClick}>
<Plus className="w-3.5 h-3.5 mr-1" />
Создать задачу
</Button>

View File

@@ -965,15 +965,15 @@ const UsersPage = () => {
</div>
{hasPermission(user, 'users.manage') && (
<div className="flex items-center gap-2">
<Button onClick={() => setLocation('/users/fields/edit')} variant="ghost" size="sm" className="h-7 px-2 text-xs">
<Button onClick={() => setLocation('/users/fields/edit')} variant="ghost" size="sm" className="px-2">
<Settings2 className="w-3.5 h-3.5 mr-1.5" />
Поля профиля
</Button>
<Button onClick={() => setInviteModalOpen(true)} variant="outline" className="h-7 px-2.5 text-xs">
<Button onClick={() => setInviteModalOpen(true)} variant="outline" size="sm">
<Send className="w-3.5 h-3.5 mr-1.5" />
Пригласить
</Button>
<Button onClick={() => setUserModalOpen(true)} className="h-7 px-2.5 text-xs" data-testid="button-add-user">
<Button onClick={() => setUserModalOpen(true)} size="sm" data-testid="button-add-user">
<Plus className="w-3.5 h-3.5 mr-1.5" />
Пользователь
</Button>
@@ -1280,7 +1280,7 @@ const UsersPage = () => {
<div className="flex items-center justify-between mb-3">
<p className="text-xs text-muted-foreground">Иерархия ролей организации</p>
{hasPermission(user, 'roles.manage') && (
<Button onClick={() => openCreateRole()} className="h-7 px-2.5 text-xs">
<Button onClick={() => openCreateRole()} size="sm">
<Plus className="w-3.5 h-3.5 mr-1.5" />
Создать роль
</Button>
@@ -1337,7 +1337,7 @@ const UsersPage = () => {
<div className="flex items-center justify-between mb-3">
<p className="text-xs text-muted-foreground">Статусы пользователей организации</p>
{hasPermission(user, 'users.manage') && (
<Button onClick={openCreateStatus} className="h-7 px-2.5 text-xs">
<Button onClick={openCreateStatus} size="sm">
<Plus className="w-3.5 h-3.5 mr-1.5" />
Создать статус
</Button>
@@ -1397,7 +1397,7 @@ const UsersPage = () => {
<Label className="text-xs">Email приглашаемого</Label>
<div className="flex gap-2">
<Input type="email" placeholder="email@example.com" value={inviteEmail} onChange={(e) => setInviteEmail(e.target.value)} className="h-7 text-xs" />
<Button onClick={handleCreateInvitation} disabled={inviteLoading || !inviteEmail.trim()} className="h-7 px-2.5 text-xs">
<Button onClick={handleCreateInvitation} disabled={inviteLoading || !inviteEmail.trim()} size="sm">
{inviteLoading ? 'Создание...' : 'Создать'}
</Button>
</div>

View File

@@ -238,6 +238,15 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
};
}, [skinId, theme, customSkinsMap]);
// Тема 2.0: data-атрибуты на <html> для точечной CSS-донастройки
// (data-density, data-control-variant, data-button-radius) — рядом с data-skin/data-theme
useLayoutEffect(() => {
const root = document.documentElement;
root.setAttribute('data-density', skinComponents.density.preset);
root.setAttribute('data-control-variant', skinComponents.control.variant);
root.setAttribute('data-button-radius', skinComponents.button.radius);
}, [skinComponents]);
const value: ThemeContextType = {
theme,
setTheme,