@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,13 @@
|
|
|
1
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("animate-pulse rounded-md bg-accent", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useTheme } from "@icomponents/react/context/theme-provider";
|
|
2
|
+
import { Toaster as Sonner } from "sonner";
|
|
3
|
+
import type { ToasterProps } from "sonner";
|
|
4
|
+
|
|
5
|
+
// Re-export the imperative toast API so consumers can fire toasts through the ui package they already
|
|
6
|
+
// depend on, without adding a direct sonner dependency.
|
|
7
|
+
export { toast } from "sonner";
|
|
8
|
+
|
|
9
|
+
export function Toaster({ ...props }: ToasterProps) {
|
|
10
|
+
const { theme = "system" } = useTheme();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Sonner
|
|
14
|
+
theme={theme as ToasterProps["theme"]}
|
|
15
|
+
className="toaster group [&_div[data-content]]:w-full"
|
|
16
|
+
style={
|
|
17
|
+
{
|
|
18
|
+
"--normal-bg": "var(--popover)",
|
|
19
|
+
"--normal-text": "var(--popover-foreground)",
|
|
20
|
+
"--normal-border": "var(--border)",
|
|
21
|
+
} as React.CSSProperties
|
|
22
|
+
}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
|
|
2
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
|
6
|
+
return (
|
|
7
|
+
<SwitchPrimitive.Root
|
|
8
|
+
data-slot="switch"
|
|
9
|
+
className={cn(
|
|
10
|
+
"peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<SwitchPrimitive.Thumb
|
|
16
|
+
data-slot="switch-thumb"
|
|
17
|
+
className={cn(
|
|
18
|
+
"pointer-events-none block size-4 rounded-full bg-background ring-0 transition-transform data-checked:translate-x-[calc(100%-2px)] data-unchecked:translate-x-0 rtl:data-checked:-translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground dark:data-unchecked:bg-foreground"
|
|
19
|
+
)}
|
|
20
|
+
/>
|
|
21
|
+
</SwitchPrimitive.Root>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { Switch };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
5
|
+
return (
|
|
6
|
+
<div data-slot="table-container" className="relative w-full overflow-x-auto">
|
|
7
|
+
<table
|
|
8
|
+
data-slot="table"
|
|
9
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
10
|
+
{...props}
|
|
11
|
+
/>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
17
|
+
return <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
21
|
+
return (
|
|
22
|
+
<tbody
|
|
23
|
+
data-slot="table-body"
|
|
24
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
31
|
+
return (
|
|
32
|
+
<tfoot
|
|
33
|
+
data-slot="table-footer"
|
|
34
|
+
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
41
|
+
return (
|
|
42
|
+
<tr
|
|
43
|
+
data-slot="table-row"
|
|
44
|
+
className={cn(
|
|
45
|
+
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
54
|
+
return (
|
|
55
|
+
<th
|
|
56
|
+
data-slot="table-head"
|
|
57
|
+
className={cn(
|
|
58
|
+
"h-10 px-2 text-start align-middle font-medium whitespace-nowrap text-foreground *:[[role=checkbox]]:translate-y-0.5",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
67
|
+
return (
|
|
68
|
+
<td
|
|
69
|
+
data-slot="table-cell"
|
|
70
|
+
className={cn(
|
|
71
|
+
"p-2 align-middle whitespace-nowrap *:[[role=checkbox]]:translate-y-0.5",
|
|
72
|
+
className
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
|
|
80
|
+
return (
|
|
81
|
+
<caption
|
|
82
|
+
data-slot="table-caption"
|
|
83
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
2
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
6
|
+
return (
|
|
7
|
+
<TabsPrimitive.Root
|
|
8
|
+
data-slot="tabs"
|
|
9
|
+
className={cn("flex flex-col gap-2", className)}
|
|
10
|
+
{...props}
|
|
11
|
+
/>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
16
|
+
return (
|
|
17
|
+
<TabsPrimitive.List
|
|
18
|
+
data-slot="tabs-list"
|
|
19
|
+
className={cn(
|
|
20
|
+
"inline-flex h-9 w-fit items-center justify-center rounded-lg bg-muted p-0.75 text-muted-foreground",
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Tab>) {
|
|
29
|
+
return (
|
|
30
|
+
<TabsPrimitive.Tab
|
|
31
|
+
data-slot="tabs-trigger"
|
|
32
|
+
className={cn(
|
|
33
|
+
"inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-active:bg-background data-active:shadow-sm dark:text-muted-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Panel>) {
|
|
42
|
+
return (
|
|
43
|
+
<TabsPrimitive.Panel
|
|
44
|
+
data-slot="tabs-content"
|
|
45
|
+
className={cn("flex-1 outline-none", className)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
5
|
+
return (
|
|
6
|
+
<textarea
|
|
7
|
+
data-slot="textarea"
|
|
8
|
+
className={cn(
|
|
9
|
+
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground 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 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { Textarea };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
|
|
4
|
+
import { cn } from "@icomponents/react/lib/utils";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
function TooltipProvider({
|
|
8
|
+
delay = 0,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
11
|
+
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delay={delay} {...props} />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
15
|
+
return (
|
|
16
|
+
<TooltipProvider>
|
|
17
|
+
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
18
|
+
</TooltipProvider>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
23
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function TooltipContent({
|
|
27
|
+
className,
|
|
28
|
+
sideOffset = 0,
|
|
29
|
+
side,
|
|
30
|
+
align,
|
|
31
|
+
alignOffset,
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Popup> &
|
|
35
|
+
Pick<
|
|
36
|
+
React.ComponentProps<typeof TooltipPrimitive.Positioner>,
|
|
37
|
+
"side" | "align" | "sideOffset" | "alignOffset"
|
|
38
|
+
>) {
|
|
39
|
+
return (
|
|
40
|
+
<TooltipPrimitive.Portal>
|
|
41
|
+
{/* Radix Content → Portal > Positioner > Popup; positioning moves to Positioner. */}
|
|
42
|
+
<TooltipPrimitive.Positioner
|
|
43
|
+
side={side}
|
|
44
|
+
align={align}
|
|
45
|
+
sideOffset={sideOffset}
|
|
46
|
+
alignOffset={alignOffset}
|
|
47
|
+
className="isolate z-50"
|
|
48
|
+
>
|
|
49
|
+
<TooltipPrimitive.Popup
|
|
50
|
+
data-slot="tooltip-content"
|
|
51
|
+
className={cn(
|
|
52
|
+
"z-50 w-fit origin-(--transform-origin) animate-in rounded-md bg-primary px-3 py-1.5 text-xs text-balance text-primary-foreground fade-in-0 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 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-primary fill-primary" />
|
|
59
|
+
</TooltipPrimitive.Popup>
|
|
60
|
+
</TooltipPrimitive.Positioner>
|
|
61
|
+
</TooltipPrimitive.Portal>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DirectionProvider as BaseDirProvider } from "@base-ui/react/direction-provider";
|
|
2
|
+
import { getCookie, setCookie, removeCookie } from "@icomponents/react/lib/cookies";
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export type Direction = "ltr" | "rtl";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DIRECTION = "ltr";
|
|
8
|
+
const DIRECTION_COOKIE_NAME = "dir";
|
|
9
|
+
const DIRECTION_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
|
10
|
+
|
|
11
|
+
type DirectionContextType = {
|
|
12
|
+
defaultDir: Direction;
|
|
13
|
+
dir: Direction;
|
|
14
|
+
setDir: (dir: Direction) => void;
|
|
15
|
+
resetDir: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DirectionContext = createContext<DirectionContextType | null>(null);
|
|
19
|
+
|
|
20
|
+
export function DirectionProvider({ children }: { children: React.ReactNode }) {
|
|
21
|
+
const [dir, _setDir] = useState<Direction>(
|
|
22
|
+
() => (getCookie(DIRECTION_COOKIE_NAME) as Direction) || DEFAULT_DIRECTION
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const htmlElement = document.documentElement;
|
|
27
|
+
htmlElement.setAttribute("dir", dir);
|
|
28
|
+
}, [dir]);
|
|
29
|
+
|
|
30
|
+
const setDir = (dir: Direction) => {
|
|
31
|
+
_setDir(dir);
|
|
32
|
+
setCookie(DIRECTION_COOKIE_NAME, dir, DIRECTION_COOKIE_MAX_AGE);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const resetDir = () => {
|
|
36
|
+
_setDir(DEFAULT_DIRECTION);
|
|
37
|
+
removeCookie(DIRECTION_COOKIE_NAME);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<DirectionContext
|
|
42
|
+
value={{
|
|
43
|
+
defaultDir: DEFAULT_DIRECTION,
|
|
44
|
+
dir,
|
|
45
|
+
setDir,
|
|
46
|
+
resetDir,
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<BaseDirProvider direction={dir}>{children}</BaseDirProvider>
|
|
50
|
+
</DirectionContext>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
55
|
+
export function useDirection() {
|
|
56
|
+
const context = useContext(DirectionContext);
|
|
57
|
+
if (!context) {
|
|
58
|
+
throw new Error("useDirection must be used within a DirectionProvider");
|
|
59
|
+
}
|
|
60
|
+
return context;
|
|
61
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { fonts as defaultFonts } from "@icomponents/react/config/fonts";
|
|
2
|
+
import { getCookie, setCookie, removeCookie } from "@icomponents/react/lib/cookies";
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
type Font = string;
|
|
6
|
+
|
|
7
|
+
const FONT_COOKIE_NAME = "font";
|
|
8
|
+
const FONT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
|
9
|
+
|
|
10
|
+
type FontContextType = {
|
|
11
|
+
font: Font;
|
|
12
|
+
/** The list this deployment offers — the appearance screen renders THESE, not core's defaults. */
|
|
13
|
+
fonts: readonly Font[];
|
|
14
|
+
setFont: (font: Font) => void;
|
|
15
|
+
resetFont: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const FontContext = createContext<FontContextType | null>(null);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `fonts` is the PRODUCT's type set: a derived project re-themed from a design prototype ships its
|
|
22
|
+
* own faces (see docs/theming-from-lynx.md) and must be able to say so WITHOUT editing
|
|
23
|
+
* `config/fonts.ts` — that file is core, and the next `sync-core.sh` would overwrite the edit.
|
|
24
|
+
* Omitted → core's default set. `fonts[0]` is the default face.
|
|
25
|
+
*/
|
|
26
|
+
export function FontProvider({
|
|
27
|
+
children,
|
|
28
|
+
fonts = defaultFonts,
|
|
29
|
+
}: {
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
fonts?: readonly Font[];
|
|
32
|
+
}) {
|
|
33
|
+
const [font, _setFont] = useState<Font>(() => {
|
|
34
|
+
const savedFont = getCookie(FONT_COOKIE_NAME);
|
|
35
|
+
return savedFont && fonts.includes(savedFont) ? savedFont : (fonts[0] as Font);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const applyFont = (font: string) => {
|
|
40
|
+
const root = document.documentElement;
|
|
41
|
+
root.classList.forEach((cls) => {
|
|
42
|
+
if (cls.startsWith("font-")) root.classList.remove(cls);
|
|
43
|
+
});
|
|
44
|
+
root.classList.add(`font-${font}`);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
applyFont(font);
|
|
48
|
+
}, [font]);
|
|
49
|
+
|
|
50
|
+
const setFont = (font: Font) => {
|
|
51
|
+
setCookie(FONT_COOKIE_NAME, font, FONT_COOKIE_MAX_AGE);
|
|
52
|
+
_setFont(font);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const resetFont = () => {
|
|
56
|
+
removeCookie(FONT_COOKIE_NAME);
|
|
57
|
+
_setFont(fonts[0] as Font);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return <FontContext value={{ font, fonts, setFont, resetFont }}>{children}</FontContext>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
64
|
+
export const useFont = () => {
|
|
65
|
+
const context = useContext(FontContext);
|
|
66
|
+
if (!context) {
|
|
67
|
+
throw new Error("useFont must be used within a FontProvider");
|
|
68
|
+
}
|
|
69
|
+
return context;
|
|
70
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { LOCALES, type Locale } from "@icomponents/react/i18n/types";
|
|
2
|
+
import { getCookie, removeCookie, setCookie } from "@icomponents/react/lib/cookies";
|
|
3
|
+
import type { i18n as I18n } from "i18next";
|
|
4
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
5
|
+
import { I18nextProvider } from "react-i18next";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Провайдер языка — по образцу DirectionProvider (кука + атрибут на <html> + контекст).
|
|
9
|
+
*
|
|
10
|
+
* Инстанс i18next создаётся в composition root (там собраны бандлы всех фич) и передаётся сюда: сам
|
|
11
|
+
* провайдер механизмом сборки словарей не занимается, он лишь хранит выбранный язык, синхронизирует
|
|
12
|
+
* его с i18next и `<html lang>`, и раздаёт переключатель через контекст.
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_LOCALE: Locale = "ru"; // основной язык проекта; en доступен переключателем
|
|
15
|
+
const LANG_COOKIE_NAME = "lang";
|
|
16
|
+
const LANG_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
17
|
+
|
|
18
|
+
export function readInitialLocale(): Locale {
|
|
19
|
+
const saved = getCookie(LANG_COOKIE_NAME) as Locale | undefined;
|
|
20
|
+
return saved && (LOCALES as readonly string[]).includes(saved) ? saved : DEFAULT_LOCALE;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type LanguageContextType = {
|
|
24
|
+
defaultLang: Locale;
|
|
25
|
+
lang: Locale;
|
|
26
|
+
langs: readonly Locale[];
|
|
27
|
+
setLang: (lang: Locale) => void;
|
|
28
|
+
resetLang: () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const LanguageContext = createContext<LanguageContextType | null>(null);
|
|
32
|
+
|
|
33
|
+
export function LanguageProvider({ i18n, children }: { i18n: I18n; children: React.ReactNode }) {
|
|
34
|
+
const [lang, _setLang] = useState<Locale>(() => (i18n.language as Locale) || DEFAULT_LOCALE);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
document.documentElement.setAttribute("lang", lang);
|
|
38
|
+
if (i18n.language !== lang) void i18n.changeLanguage(lang);
|
|
39
|
+
}, [lang, i18n]);
|
|
40
|
+
|
|
41
|
+
const setLang = (next: Locale) => {
|
|
42
|
+
_setLang(next);
|
|
43
|
+
setCookie(LANG_COOKIE_NAME, next, LANG_COOKIE_MAX_AGE);
|
|
44
|
+
};
|
|
45
|
+
const resetLang = () => {
|
|
46
|
+
_setLang(DEFAULT_LOCALE);
|
|
47
|
+
removeCookie(LANG_COOKIE_NAME);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<LanguageContext.Provider
|
|
52
|
+
value={{ defaultLang: DEFAULT_LOCALE, lang, langs: LOCALES, setLang, resetLang }}
|
|
53
|
+
>
|
|
54
|
+
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
|
|
55
|
+
</LanguageContext.Provider>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useLanguage(): LanguageContextType {
|
|
60
|
+
const ctx = useContext(LanguageContext);
|
|
61
|
+
if (!ctx) throw new Error("useLanguage must be used within a LanguageProvider");
|
|
62
|
+
return ctx;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Как `useLanguage`, но НЕ бросает без провайдера — возвращает `null`.
|
|
67
|
+
*
|
|
68
|
+
* Нужен core-компонентам (config-drawer), которые синкаются в продукт РАНЬШЕ, чем продукт успел
|
|
69
|
+
* обернуть приложение в LanguageProvider: они просто прячут секцию языка, а не падают.
|
|
70
|
+
*/
|
|
71
|
+
export function useOptionalLanguage(): LanguageContextType | null {
|
|
72
|
+
return useContext(LanguageContext);
|
|
73
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { getCookie, setCookie } from "@icomponents/react/lib/cookies";
|
|
2
|
+
import { createContext, useContext, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export type Collapsible = "offcanvas" | "icon" | "none";
|
|
5
|
+
type Variant = "inset" | "sidebar" | "floating";
|
|
6
|
+
|
|
7
|
+
// Cookie constants following the pattern from sidebar.tsx
|
|
8
|
+
const LAYOUT_COLLAPSIBLE_COOKIE_NAME = "layout_collapsible";
|
|
9
|
+
const LAYOUT_VARIANT_COOKIE_NAME = "layout_variant";
|
|
10
|
+
const LAYOUT_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
|
|
11
|
+
|
|
12
|
+
// Default values
|
|
13
|
+
const DEFAULT_VARIANT = "inset";
|
|
14
|
+
const DEFAULT_COLLAPSIBLE = "icon";
|
|
15
|
+
|
|
16
|
+
type LayoutContextType = {
|
|
17
|
+
resetLayout: () => void;
|
|
18
|
+
|
|
19
|
+
defaultCollapsible: Collapsible;
|
|
20
|
+
collapsible: Collapsible;
|
|
21
|
+
setCollapsible: (collapsible: Collapsible) => void;
|
|
22
|
+
|
|
23
|
+
defaultVariant: Variant;
|
|
24
|
+
variant: Variant;
|
|
25
|
+
setVariant: (variant: Variant) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const LayoutContext = createContext<LayoutContextType | null>(null);
|
|
29
|
+
|
|
30
|
+
type LayoutProviderProps = {
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function LayoutProvider({ children }: LayoutProviderProps) {
|
|
35
|
+
const [collapsible, _setCollapsible] = useState<Collapsible>(() => {
|
|
36
|
+
const saved = getCookie(LAYOUT_COLLAPSIBLE_COOKIE_NAME);
|
|
37
|
+
return (saved as Collapsible) || DEFAULT_COLLAPSIBLE;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const [variant, _setVariant] = useState<Variant>(() => {
|
|
41
|
+
const saved = getCookie(LAYOUT_VARIANT_COOKIE_NAME);
|
|
42
|
+
return (saved as Variant) || DEFAULT_VARIANT;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const setCollapsible = (newCollapsible: Collapsible) => {
|
|
46
|
+
_setCollapsible(newCollapsible);
|
|
47
|
+
setCookie(LAYOUT_COLLAPSIBLE_COOKIE_NAME, newCollapsible, LAYOUT_COOKIE_MAX_AGE);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const setVariant = (newVariant: Variant) => {
|
|
51
|
+
_setVariant(newVariant);
|
|
52
|
+
setCookie(LAYOUT_VARIANT_COOKIE_NAME, newVariant, LAYOUT_COOKIE_MAX_AGE);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const resetLayout = () => {
|
|
56
|
+
setCollapsible(DEFAULT_COLLAPSIBLE);
|
|
57
|
+
setVariant(DEFAULT_VARIANT);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const contextValue: LayoutContextType = {
|
|
61
|
+
resetLayout,
|
|
62
|
+
defaultCollapsible: DEFAULT_COLLAPSIBLE,
|
|
63
|
+
collapsible,
|
|
64
|
+
setCollapsible,
|
|
65
|
+
defaultVariant: DEFAULT_VARIANT,
|
|
66
|
+
variant,
|
|
67
|
+
setVariant,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return <LayoutContext value={contextValue}>{children}</LayoutContext>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Define the hook for the provider
|
|
74
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
75
|
+
export function useLayout() {
|
|
76
|
+
const context = useContext(LayoutContext);
|
|
77
|
+
if (!context) {
|
|
78
|
+
throw new Error("useLayout must be used within a LayoutProvider");
|
|
79
|
+
}
|
|
80
|
+
return context;
|
|
81
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CommandMenu } from "@icomponents/react/components/command-menu";
|
|
2
|
+
import { type NavGroup } from "@icomponents/react/components/layout/types";
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
type SearchContextType = {
|
|
6
|
+
open: boolean;
|
|
7
|
+
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const SearchContext = createContext<SearchContextType | null>(null);
|
|
11
|
+
|
|
12
|
+
type SearchProviderProps = {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
navGroups: NavGroup[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function SearchProvider({ children, navGroups }: SearchProviderProps) {
|
|
18
|
+
const [open, setOpen] = useState(false);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const down = (e: KeyboardEvent) => {
|
|
22
|
+
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
setOpen((open) => !open);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
document.addEventListener("keydown", down);
|
|
28
|
+
return () => document.removeEventListener("keydown", down);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<SearchContext value={{ open, setOpen }}>
|
|
33
|
+
{children}
|
|
34
|
+
<CommandMenu navGroups={navGroups} />
|
|
35
|
+
</SearchContext>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
40
|
+
export const useSearch = () => {
|
|
41
|
+
const searchContext = useContext(SearchContext);
|
|
42
|
+
|
|
43
|
+
if (!searchContext) {
|
|
44
|
+
throw new Error("useSearch has to be used within SearchProvider");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return searchContext;
|
|
48
|
+
};
|