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:
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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 ? 'Создание...' : 'Создать'}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user