@icomponents/react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/package.json +165 -0
- package/src/assets/custom/icon-dir.tsx +105 -0
- package/src/assets/custom/icon-layout-compact.tsx +84 -0
- package/src/assets/custom/icon-layout-default.tsx +48 -0
- package/src/assets/custom/icon-layout-full.tsx +68 -0
- package/src/assets/custom/icon-sidebar-floating.tsx +62 -0
- package/src/assets/custom/icon-sidebar-inset.tsx +53 -0
- package/src/assets/custom/icon-sidebar-sidebar.tsx +48 -0
- package/src/assets/custom/icon-theme-dark.tsx +36 -0
- package/src/assets/custom/icon-theme-light.tsx +35 -0
- package/src/assets/custom/icon-theme-system.tsx +113 -0
- package/src/components/admin-page-header.tsx +29 -0
- package/src/components/color-theme-switch.tsx +93 -0
- package/src/components/coming-soon.tsx +16 -0
- package/src/components/command-menu.tsx +98 -0
- package/src/components/config-drawer.tsx +401 -0
- package/src/components/confirm-dialog.dom.test.tsx +60 -0
- package/src/components/confirm-dialog.tsx +75 -0
- package/src/components/data-table/bulk-actions.tsx +193 -0
- package/src/components/data-table/column-header.tsx +66 -0
- package/src/components/data-table/data-table.dom.test.tsx +54 -0
- package/src/components/data-table/data-table.tsx +189 -0
- package/src/components/data-table/faceted-filter.tsx +156 -0
- package/src/components/data-table/index.ts +5 -0
- package/src/components/data-table/pagination.tsx +127 -0
- package/src/components/data-table/tanstack-table.d.ts +12 -0
- package/src/components/data-table/toolbar.tsx +88 -0
- package/src/components/data-table/view-options.tsx +49 -0
- package/src/components/date-picker.tsx +39 -0
- package/src/components/detail-card.tsx +63 -0
- package/src/components/language-switch.tsx +38 -0
- package/src/components/layout/app-sidebar.tsx +59 -0
- package/src/components/layout/app-title.tsx +62 -0
- package/src/components/layout/header.tsx +61 -0
- package/src/components/layout/main.tsx +34 -0
- package/src/components/layout/nav-group.tsx +185 -0
- package/src/components/layout/nav-user.tsx +115 -0
- package/src/components/layout/screen-header.tsx +119 -0
- package/src/components/layout/team-switcher.tsx +84 -0
- package/src/components/layout/top-nav.tsx +68 -0
- package/src/components/layout/types.ts +44 -0
- package/src/components/learn-more.tsx +35 -0
- package/src/components/long-text.tsx +80 -0
- package/src/components/navigation-progress.tsx +18 -0
- package/src/components/password-input.tsx +36 -0
- package/src/components/profile-dropdown.tsx +106 -0
- package/src/components/search.tsx +38 -0
- package/src/components/select-dropdown.tsx +62 -0
- package/src/components/sign-out-dialog.tsx +31 -0
- package/src/components/skip-to-main.tsx +10 -0
- package/src/components/stat-tile.tsx +56 -0
- package/src/components/status-badge.test.ts +23 -0
- package/src/components/status-badge.tsx +49 -0
- package/src/components/theme-switch.tsx +61 -0
- package/src/components/ui/alert-dialog.tsx +134 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +38 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/base-ui-state-attrs.dom.test.tsx +80 -0
- package/src/components/ui/button.tsx +51 -0
- package/src/components/ui/calendar.tsx +174 -0
- package/src/components/ui/card.tsx +74 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +18 -0
- package/src/components/ui/command.tsx +160 -0
- package/src/components/ui/dialog.tsx +129 -0
- package/src/components/ui/dropdown-menu.tsx +254 -0
- package/src/components/ui/form.tsx +155 -0
- package/src/components/ui/input-otp.tsx +67 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/popover.tsx +54 -0
- package/src/components/ui/radio-group.tsx +37 -0
- package/src/components/ui/scroll-area.tsx +57 -0
- package/src/components/ui/select.tsx +213 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +128 -0
- package/src/components/ui/sidebar.tsx +702 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +26 -0
- package/src/components/ui/switch.tsx +25 -0
- package/src/components/ui/table.tsx +89 -0
- package/src/components/ui/tabs.tsx +51 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/config/fonts.ts +2 -0
- package/src/context/direction-provider.tsx +61 -0
- package/src/context/font-provider.tsx +70 -0
- package/src/context/language-provider.tsx +73 -0
- package/src/context/layout-provider.tsx +81 -0
- package/src/context/search-provider.tsx +48 -0
- package/src/context/theme-provider.tsx +108 -0
- package/src/hooks/sort-url.test.ts +16 -0
- package/src/hooks/use-dialog-state.tsx +15 -0
- package/src/hooks/use-mobile.tsx +16 -0
- package/src/hooks/use-table-url-state.ts +243 -0
- package/src/i18n/common.ts +164 -0
- package/src/i18n/create-i18n.test.ts +50 -0
- package/src/i18n/create-i18n.ts +67 -0
- package/src/i18n/types.ts +22 -0
- package/src/i18n/use-translation.ts +6 -0
- package/src/lib/clipboard.test.ts +46 -0
- package/src/lib/clipboard.ts +29 -0
- package/src/lib/cookies.ts +39 -0
- package/src/lib/format-cell.test.ts +57 -0
- package/src/lib/format-cell.ts +63 -0
- package/src/lib/handle-server-error.test.ts +85 -0
- package/src/lib/handle-server-error.ts +39 -0
- package/src/lib/random-id.ts +13 -0
- package/src/lib/show-submitted-data.tsx +14 -0
- package/src/lib/utils.test.ts +35 -0
- package/src/lib/utils.ts +78 -0
- package/src/secure-context.contract.test.ts +63 -0
- package/src/testids.contract.test.ts +113 -0
- package/src/testids.ts +8 -0
- package/src/theme-contract.test.ts +72 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
|
|
2
|
+
import { buttonVariants } from "@icomponents/react/components/ui/button";
|
|
3
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
|
7
|
+
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function AlertDialogTrigger({
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
|
13
|
+
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
|
17
|
+
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function AlertDialogOverlay({
|
|
21
|
+
className,
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Backdrop>) {
|
|
24
|
+
return (
|
|
25
|
+
<AlertDialogPrimitive.Backdrop
|
|
26
|
+
data-slot="alert-dialog-overlay"
|
|
27
|
+
className={cn(
|
|
28
|
+
"fixed inset-0 z-50 bg-black/50 data-closed:animate-out data-closed:fade-out-0 data-open:animate-in data-open:fade-in-0",
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function AlertDialogContent({
|
|
37
|
+
className,
|
|
38
|
+
...props
|
|
39
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Popup>) {
|
|
40
|
+
return (
|
|
41
|
+
<AlertDialogPortal>
|
|
42
|
+
<AlertDialogOverlay />
|
|
43
|
+
<AlertDialogPrimitive.Popup
|
|
44
|
+
data-slot="alert-dialog-content"
|
|
45
|
+
className={cn(
|
|
46
|
+
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 sm:max-w-lg",
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
</AlertDialogPortal>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
data-slot="alert-dialog-header"
|
|
59
|
+
className={cn("flex flex-col gap-2 text-center sm:text-start", className)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
data-slot="alert-dialog-footer"
|
|
69
|
+
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function AlertDialogTitle({
|
|
76
|
+
className,
|
|
77
|
+
...props
|
|
78
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
|
79
|
+
return (
|
|
80
|
+
<AlertDialogPrimitive.Title
|
|
81
|
+
data-slot="alert-dialog-title"
|
|
82
|
+
className={cn("text-lg font-semibold", className)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function AlertDialogDescription({
|
|
89
|
+
className,
|
|
90
|
+
...props
|
|
91
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
|
92
|
+
return (
|
|
93
|
+
<AlertDialogPrimitive.Description
|
|
94
|
+
data-slot="alert-dialog-description"
|
|
95
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Base UI AlertDialog has no Action part — reuse Close (click fires, then the dialog closes),
|
|
102
|
+
// styled as the primary button. Same close-on-click semantics as Radix Action.
|
|
103
|
+
function AlertDialogAction({
|
|
104
|
+
className,
|
|
105
|
+
...props
|
|
106
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Close>) {
|
|
107
|
+
return <AlertDialogPrimitive.Close className={cn(buttonVariants(), className)} {...props} />;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function AlertDialogCancel({
|
|
111
|
+
className,
|
|
112
|
+
...props
|
|
113
|
+
}: React.ComponentProps<typeof AlertDialogPrimitive.Close>) {
|
|
114
|
+
return (
|
|
115
|
+
<AlertDialogPrimitive.Close
|
|
116
|
+
className={cn(buttonVariants({ variant: "outline" }), className)}
|
|
117
|
+
{...props}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
AlertDialog,
|
|
124
|
+
AlertDialogPortal,
|
|
125
|
+
AlertDialogOverlay,
|
|
126
|
+
AlertDialogTrigger,
|
|
127
|
+
AlertDialogContent,
|
|
128
|
+
AlertDialogHeader,
|
|
129
|
+
AlertDialogFooter,
|
|
130
|
+
AlertDialogTitle,
|
|
131
|
+
AlertDialogDescription,
|
|
132
|
+
AlertDialogAction,
|
|
133
|
+
AlertDialogCancel,
|
|
134
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
const alertVariants = cva(
|
|
6
|
+
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "bg-card text-card-foreground",
|
|
11
|
+
destructive:
|
|
12
|
+
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
variant: "default",
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function Alert({
|
|
22
|
+
className,
|
|
23
|
+
variant,
|
|
24
|
+
...props
|
|
25
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
data-slot="alert"
|
|
29
|
+
role="alert"
|
|
30
|
+
className={cn(alertVariants({ variant }), className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
data-slot="alert-title"
|
|
40
|
+
className={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
data-slot="alert-description"
|
|
50
|
+
className={cn(
|
|
51
|
+
"col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed",
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { Alert, AlertTitle, AlertDescription };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
|
|
2
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
function Avatar({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
|
6
|
+
return (
|
|
7
|
+
<AvatarPrimitive.Root
|
|
8
|
+
data-slot="avatar"
|
|
9
|
+
className={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
|
|
10
|
+
{...props}
|
|
11
|
+
/>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
16
|
+
return (
|
|
17
|
+
<AvatarPrimitive.Image
|
|
18
|
+
data-slot="avatar-image"
|
|
19
|
+
className={cn("aspect-square size-full", className)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function AvatarFallback({
|
|
26
|
+
className,
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
29
|
+
return (
|
|
30
|
+
<AvatarPrimitive.Fallback
|
|
31
|
+
data-slot="avatar-fallback"
|
|
32
|
+
className={cn("flex size-full items-center justify-center rounded-full bg-muted", className)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { Avatar, AvatarImage, AvatarFallback };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
12
|
+
secondary:
|
|
13
|
+
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
14
|
+
destructive:
|
|
15
|
+
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
16
|
+
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
variant: "default",
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Slot/asChild polymorphism → Base UI `useRender` (per migrate-radix-to-base). Default element is a
|
|
26
|
+
// <span>; pass `render` to compose (e.g. an <a> for a link badge).
|
|
27
|
+
function Badge({
|
|
28
|
+
className,
|
|
29
|
+
variant,
|
|
30
|
+
render,
|
|
31
|
+
...props
|
|
32
|
+
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
33
|
+
return useRender({
|
|
34
|
+
render: render ?? <span data-slot="badge" />,
|
|
35
|
+
props: mergeProps<"span">({ className: cn(badgeVariants({ variant }), className) }, props),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
4
|
+
import { userEvent } from "@testing-library/user-event";
|
|
5
|
+
|
|
6
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./collapsible";
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from "./dropdown-menu";
|
|
13
|
+
import { Switch } from "./switch";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Контракт data-атрибутов Base UI — то, на что цепляются Tailwind-варианты во ВСЁМ пакете.
|
|
17
|
+
*
|
|
18
|
+
* Зачем этот тест существует. Открытое/закрытое состояние в Base UI выражается атрибутом-флагом
|
|
19
|
+
* (`data-popup-open`, `data-panel-open`, `data-checked`), а НЕ радиксовым `data-state="open"`.
|
|
20
|
+
* Разница не видна ни компилятору, ни обычному DOM-тесту: класс `data-[state=open]:bg-accent`
|
|
21
|
+
* прекрасно компилируется, просто никогда ни к чему не применяется. Такая опечатка не падает —
|
|
22
|
+
* она тихо гасит подсветку открытого меню, поворот шеврона, активный пункт сайдбара.
|
|
23
|
+
*
|
|
24
|
+
* Именно это и случилось при переносе с Radix: пять селекторов пережили порт мёртвыми.
|
|
25
|
+
* Тест фиксирует имена атрибутов, чтобы следующий апгрейд Base UI (или следующий порт) ломался
|
|
26
|
+
* ГРОМКО — здесь, а не через месяц в глазах пользователя.
|
|
27
|
+
*/
|
|
28
|
+
afterEach(cleanup);
|
|
29
|
+
|
|
30
|
+
describe("Base UI: контракт атрибутов состояния", () => {
|
|
31
|
+
it("триггер меню помечается data-popup-open (а не data-state=open)", async () => {
|
|
32
|
+
render(
|
|
33
|
+
<DropdownMenu>
|
|
34
|
+
<DropdownMenuTrigger data-testid="trigger">меню</DropdownMenuTrigger>
|
|
35
|
+
<DropdownMenuContent>
|
|
36
|
+
<DropdownMenuItem>пункт</DropdownMenuItem>
|
|
37
|
+
</DropdownMenuContent>
|
|
38
|
+
</DropdownMenu>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const trigger = screen.getByTestId("trigger");
|
|
42
|
+
expect(trigger.hasAttribute("data-popup-open")).toBe(false);
|
|
43
|
+
|
|
44
|
+
await userEvent.click(trigger);
|
|
45
|
+
|
|
46
|
+
expect(trigger.hasAttribute("data-popup-open")).toBe(true);
|
|
47
|
+
// Радиксовая форма НЕ появляется — селекторы data-[state=open] были бы мертвы.
|
|
48
|
+
expect(trigger.getAttribute("data-state")).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("триггер и панель Collapsible помечаются data-panel-open / data-open", async () => {
|
|
52
|
+
render(
|
|
53
|
+
<Collapsible>
|
|
54
|
+
<CollapsibleTrigger data-testid="trigger">раскрыть</CollapsibleTrigger>
|
|
55
|
+
<CollapsibleContent data-testid="panel">содержимое</CollapsibleContent>
|
|
56
|
+
</Collapsible>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const trigger = screen.getByTestId("trigger");
|
|
60
|
+
expect(trigger.hasAttribute("data-panel-open")).toBe(false);
|
|
61
|
+
|
|
62
|
+
await userEvent.click(trigger);
|
|
63
|
+
|
|
64
|
+
expect(trigger.hasAttribute("data-panel-open")).toBe(true);
|
|
65
|
+
expect(trigger.getAttribute("data-state")).toBeNull();
|
|
66
|
+
expect(screen.getByTestId("panel").hasAttribute("data-open")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("switch помечается data-checked / data-unchecked", async () => {
|
|
70
|
+
render(<Switch data-testid="switch" />);
|
|
71
|
+
|
|
72
|
+
const control = screen.getByTestId("switch");
|
|
73
|
+
expect(control.hasAttribute("data-unchecked")).toBe(true);
|
|
74
|
+
|
|
75
|
+
await userEvent.click(control);
|
|
76
|
+
|
|
77
|
+
expect(control.hasAttribute("data-checked")).toBe(true);
|
|
78
|
+
expect(control.getAttribute("data-state")).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
2
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
14
|
+
outline:
|
|
15
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
16
|
+
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
17
|
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
18
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
22
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
23
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
24
|
+
icon: "size-9",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
variant: "default",
|
|
29
|
+
size: "default",
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Migrated to Base UI's real Button primitive (per migrate-radix-to-base). Polymorphism is the
|
|
35
|
+
// `render` prop (Base UI) instead of Radix `asChild` — no consumer used `<Button asChild>`.
|
|
36
|
+
function Button({
|
|
37
|
+
className,
|
|
38
|
+
variant,
|
|
39
|
+
size,
|
|
40
|
+
...props
|
|
41
|
+
}: React.ComponentProps<typeof ButtonPrimitive> & VariantProps<typeof buttonVariants>) {
|
|
42
|
+
return (
|
|
43
|
+
<ButtonPrimitive
|
|
44
|
+
data-slot="button"
|
|
45
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Button, buttonVariants } from "@icomponents/react/components/ui/button";
|
|
2
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
3
|
+
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
|
|
6
|
+
|
|
7
|
+
function Calendar({
|
|
8
|
+
className,
|
|
9
|
+
classNames,
|
|
10
|
+
showOutsideDays = true,
|
|
11
|
+
captionLayout = "label",
|
|
12
|
+
buttonVariant = "ghost",
|
|
13
|
+
formatters,
|
|
14
|
+
components,
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<typeof DayPicker> & {
|
|
17
|
+
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
|
|
18
|
+
}) {
|
|
19
|
+
const defaultClassNames = getDefaultClassNames();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<DayPicker
|
|
23
|
+
showOutsideDays={showOutsideDays}
|
|
24
|
+
className={cn(
|
|
25
|
+
"group/calendar bg-background p-3 [--cell-size:--spacing(8)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
|
|
26
|
+
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
|
27
|
+
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
captionLayout={captionLayout}
|
|
31
|
+
formatters={{
|
|
32
|
+
formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
|
|
33
|
+
...formatters,
|
|
34
|
+
}}
|
|
35
|
+
classNames={{
|
|
36
|
+
root: cn("w-fit", defaultClassNames.root),
|
|
37
|
+
months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
|
|
38
|
+
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
|
39
|
+
nav: cn(
|
|
40
|
+
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
|
41
|
+
defaultClassNames.nav
|
|
42
|
+
),
|
|
43
|
+
button_previous: cn(
|
|
44
|
+
buttonVariants({ variant: buttonVariant }),
|
|
45
|
+
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
|
46
|
+
defaultClassNames.button_previous
|
|
47
|
+
),
|
|
48
|
+
button_next: cn(
|
|
49
|
+
buttonVariants({ variant: buttonVariant }),
|
|
50
|
+
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
|
51
|
+
defaultClassNames.button_next
|
|
52
|
+
),
|
|
53
|
+
month_caption: cn(
|
|
54
|
+
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
|
|
55
|
+
defaultClassNames.month_caption
|
|
56
|
+
),
|
|
57
|
+
dropdowns: cn(
|
|
58
|
+
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
|
|
59
|
+
defaultClassNames.dropdowns
|
|
60
|
+
),
|
|
61
|
+
dropdown_root: cn(
|
|
62
|
+
"relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50",
|
|
63
|
+
defaultClassNames.dropdown_root
|
|
64
|
+
),
|
|
65
|
+
dropdown: cn("absolute inset-0 bg-popover opacity-0", defaultClassNames.dropdown),
|
|
66
|
+
caption_label: cn(
|
|
67
|
+
"font-medium select-none",
|
|
68
|
+
captionLayout === "label"
|
|
69
|
+
? "text-sm"
|
|
70
|
+
: "flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
|
|
71
|
+
defaultClassNames.caption_label
|
|
72
|
+
),
|
|
73
|
+
table: "w-full border-collapse",
|
|
74
|
+
weekdays: cn("flex", defaultClassNames.weekdays),
|
|
75
|
+
weekday: cn(
|
|
76
|
+
"flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none",
|
|
77
|
+
defaultClassNames.weekday
|
|
78
|
+
),
|
|
79
|
+
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
|
80
|
+
week_number_header: cn("w-(--cell-size) select-none", defaultClassNames.week_number_header),
|
|
81
|
+
week_number: cn(
|
|
82
|
+
"text-[0.8rem] text-muted-foreground select-none",
|
|
83
|
+
defaultClassNames.week_number
|
|
84
|
+
),
|
|
85
|
+
day: cn(
|
|
86
|
+
"group/day relative aspect-square h-full w-full p-0 text-center select-none [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
|
|
87
|
+
defaultClassNames.day
|
|
88
|
+
),
|
|
89
|
+
range_start: cn("rounded-l-md bg-accent", defaultClassNames.range_start),
|
|
90
|
+
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
|
91
|
+
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
|
92
|
+
today: cn(
|
|
93
|
+
"rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none",
|
|
94
|
+
defaultClassNames.today
|
|
95
|
+
),
|
|
96
|
+
outside: cn(
|
|
97
|
+
"text-muted-foreground aria-selected:text-muted-foreground",
|
|
98
|
+
defaultClassNames.outside
|
|
99
|
+
),
|
|
100
|
+
disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
|
|
101
|
+
hidden: cn("invisible", defaultClassNames.hidden),
|
|
102
|
+
...classNames,
|
|
103
|
+
}}
|
|
104
|
+
components={{
|
|
105
|
+
Root: ({ className, rootRef, ...props }) => {
|
|
106
|
+
return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;
|
|
107
|
+
},
|
|
108
|
+
Chevron: ({ className, orientation, ...props }) => {
|
|
109
|
+
if (orientation === "left") {
|
|
110
|
+
return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (orientation === "right") {
|
|
114
|
+
return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
|
|
118
|
+
},
|
|
119
|
+
DayButton: CalendarDayButton,
|
|
120
|
+
WeekNumber: ({ children, ...props }) => {
|
|
121
|
+
return (
|
|
122
|
+
<td {...props}>
|
|
123
|
+
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
|
124
|
+
{children}
|
|
125
|
+
</div>
|
|
126
|
+
</td>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
...components,
|
|
130
|
+
}}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function CalendarDayButton({
|
|
137
|
+
className,
|
|
138
|
+
day,
|
|
139
|
+
modifiers,
|
|
140
|
+
...props
|
|
141
|
+
}: React.ComponentProps<typeof DayButton>) {
|
|
142
|
+
const defaultClassNames = getDefaultClassNames();
|
|
143
|
+
|
|
144
|
+
const ref = React.useRef<HTMLButtonElement>(null);
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
if (modifiers.focused) ref.current?.focus();
|
|
147
|
+
}, [modifiers.focused]);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<Button
|
|
151
|
+
ref={ref}
|
|
152
|
+
variant="ghost"
|
|
153
|
+
size="icon"
|
|
154
|
+
data-day={day.date.toLocaleDateString()}
|
|
155
|
+
data-selected-single={
|
|
156
|
+
modifiers.selected &&
|
|
157
|
+
!modifiers.range_start &&
|
|
158
|
+
!modifiers.range_end &&
|
|
159
|
+
!modifiers.range_middle
|
|
160
|
+
}
|
|
161
|
+
data-range-start={modifiers.range_start}
|
|
162
|
+
data-range-end={modifiers.range_end}
|
|
163
|
+
data-range-middle={modifiers.range_middle}
|
|
164
|
+
className={cn(
|
|
165
|
+
"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70",
|
|
166
|
+
defaultClassNames.day,
|
|
167
|
+
className
|
|
168
|
+
)}
|
|
169
|
+
{...props}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { Calendar, CalendarDayButton };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
data-slot="card"
|
|
8
|
+
className={cn(
|
|
9
|
+
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
data-slot="card-header"
|
|
21
|
+
className={cn(
|
|
22
|
+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
data-slot="card-title"
|
|
34
|
+
className={cn("leading-none font-semibold", className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
data-slot="card-description"
|
|
44
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
data-slot="card-action"
|
|
54
|
+
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
61
|
+
return <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
data-slot="card-footer"
|
|
68
|
+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
|
2
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
3
|
+
import { CheckIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
7
|
+
return (
|
|
8
|
+
<CheckboxPrimitive.Root
|
|
9
|
+
data-slot="checkbox"
|
|
10
|
+
className={cn(
|
|
11
|
+
"peer size-4 shrink-0 rounded-lg border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-checked:bg-primary",
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<CheckboxPrimitive.Indicator
|
|
17
|
+
data-slot="checkbox-indicator"
|
|
18
|
+
className="flex items-center justify-center text-current transition-none"
|
|
19
|
+
>
|
|
20
|
+
<CheckIcon className="size-3.5" />
|
|
21
|
+
</CheckboxPrimitive.Indicator>
|
|
22
|
+
</CheckboxPrimitive.Root>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Checkbox };
|