@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.
Files changed (117) hide show
  1. package/README.md +40 -0
  2. package/package.json +165 -0
  3. package/src/assets/custom/icon-dir.tsx +105 -0
  4. package/src/assets/custom/icon-layout-compact.tsx +84 -0
  5. package/src/assets/custom/icon-layout-default.tsx +48 -0
  6. package/src/assets/custom/icon-layout-full.tsx +68 -0
  7. package/src/assets/custom/icon-sidebar-floating.tsx +62 -0
  8. package/src/assets/custom/icon-sidebar-inset.tsx +53 -0
  9. package/src/assets/custom/icon-sidebar-sidebar.tsx +48 -0
  10. package/src/assets/custom/icon-theme-dark.tsx +36 -0
  11. package/src/assets/custom/icon-theme-light.tsx +35 -0
  12. package/src/assets/custom/icon-theme-system.tsx +113 -0
  13. package/src/components/admin-page-header.tsx +29 -0
  14. package/src/components/color-theme-switch.tsx +93 -0
  15. package/src/components/coming-soon.tsx +16 -0
  16. package/src/components/command-menu.tsx +98 -0
  17. package/src/components/config-drawer.tsx +401 -0
  18. package/src/components/confirm-dialog.dom.test.tsx +60 -0
  19. package/src/components/confirm-dialog.tsx +75 -0
  20. package/src/components/data-table/bulk-actions.tsx +193 -0
  21. package/src/components/data-table/column-header.tsx +66 -0
  22. package/src/components/data-table/data-table.dom.test.tsx +54 -0
  23. package/src/components/data-table/data-table.tsx +189 -0
  24. package/src/components/data-table/faceted-filter.tsx +156 -0
  25. package/src/components/data-table/index.ts +5 -0
  26. package/src/components/data-table/pagination.tsx +127 -0
  27. package/src/components/data-table/tanstack-table.d.ts +12 -0
  28. package/src/components/data-table/toolbar.tsx +88 -0
  29. package/src/components/data-table/view-options.tsx +49 -0
  30. package/src/components/date-picker.tsx +39 -0
  31. package/src/components/detail-card.tsx +63 -0
  32. package/src/components/language-switch.tsx +38 -0
  33. package/src/components/layout/app-sidebar.tsx +59 -0
  34. package/src/components/layout/app-title.tsx +62 -0
  35. package/src/components/layout/header.tsx +61 -0
  36. package/src/components/layout/main.tsx +34 -0
  37. package/src/components/layout/nav-group.tsx +185 -0
  38. package/src/components/layout/nav-user.tsx +115 -0
  39. package/src/components/layout/screen-header.tsx +119 -0
  40. package/src/components/layout/team-switcher.tsx +84 -0
  41. package/src/components/layout/top-nav.tsx +68 -0
  42. package/src/components/layout/types.ts +44 -0
  43. package/src/components/learn-more.tsx +35 -0
  44. package/src/components/long-text.tsx +80 -0
  45. package/src/components/navigation-progress.tsx +18 -0
  46. package/src/components/password-input.tsx +36 -0
  47. package/src/components/profile-dropdown.tsx +106 -0
  48. package/src/components/search.tsx +38 -0
  49. package/src/components/select-dropdown.tsx +62 -0
  50. package/src/components/sign-out-dialog.tsx +31 -0
  51. package/src/components/skip-to-main.tsx +10 -0
  52. package/src/components/stat-tile.tsx +56 -0
  53. package/src/components/status-badge.test.ts +23 -0
  54. package/src/components/status-badge.tsx +49 -0
  55. package/src/components/theme-switch.tsx +61 -0
  56. package/src/components/ui/alert-dialog.tsx +134 -0
  57. package/src/components/ui/alert.tsx +59 -0
  58. package/src/components/ui/avatar.tsx +38 -0
  59. package/src/components/ui/badge.tsx +39 -0
  60. package/src/components/ui/base-ui-state-attrs.dom.test.tsx +80 -0
  61. package/src/components/ui/button.tsx +51 -0
  62. package/src/components/ui/calendar.tsx +174 -0
  63. package/src/components/ui/card.tsx +74 -0
  64. package/src/components/ui/checkbox.tsx +26 -0
  65. package/src/components/ui/collapsible.tsx +18 -0
  66. package/src/components/ui/command.tsx +160 -0
  67. package/src/components/ui/dialog.tsx +129 -0
  68. package/src/components/ui/dropdown-menu.tsx +254 -0
  69. package/src/components/ui/form.tsx +155 -0
  70. package/src/components/ui/input-otp.tsx +67 -0
  71. package/src/components/ui/input.tsx +20 -0
  72. package/src/components/ui/label.tsx +21 -0
  73. package/src/components/ui/popover.tsx +54 -0
  74. package/src/components/ui/radio-group.tsx +37 -0
  75. package/src/components/ui/scroll-area.tsx +57 -0
  76. package/src/components/ui/select.tsx +213 -0
  77. package/src/components/ui/separator.tsx +23 -0
  78. package/src/components/ui/sheet.tsx +128 -0
  79. package/src/components/ui/sidebar.tsx +702 -0
  80. package/src/components/ui/skeleton.tsx +13 -0
  81. package/src/components/ui/sonner.tsx +26 -0
  82. package/src/components/ui/switch.tsx +25 -0
  83. package/src/components/ui/table.tsx +89 -0
  84. package/src/components/ui/tabs.tsx +51 -0
  85. package/src/components/ui/textarea.tsx +17 -0
  86. package/src/components/ui/tooltip.tsx +65 -0
  87. package/src/config/fonts.ts +2 -0
  88. package/src/context/direction-provider.tsx +61 -0
  89. package/src/context/font-provider.tsx +70 -0
  90. package/src/context/language-provider.tsx +73 -0
  91. package/src/context/layout-provider.tsx +81 -0
  92. package/src/context/search-provider.tsx +48 -0
  93. package/src/context/theme-provider.tsx +108 -0
  94. package/src/hooks/sort-url.test.ts +16 -0
  95. package/src/hooks/use-dialog-state.tsx +15 -0
  96. package/src/hooks/use-mobile.tsx +16 -0
  97. package/src/hooks/use-table-url-state.ts +243 -0
  98. package/src/i18n/common.ts +164 -0
  99. package/src/i18n/create-i18n.test.ts +50 -0
  100. package/src/i18n/create-i18n.ts +67 -0
  101. package/src/i18n/types.ts +22 -0
  102. package/src/i18n/use-translation.ts +6 -0
  103. package/src/lib/clipboard.test.ts +46 -0
  104. package/src/lib/clipboard.ts +29 -0
  105. package/src/lib/cookies.ts +39 -0
  106. package/src/lib/format-cell.test.ts +57 -0
  107. package/src/lib/format-cell.ts +63 -0
  108. package/src/lib/handle-server-error.test.ts +85 -0
  109. package/src/lib/handle-server-error.ts +39 -0
  110. package/src/lib/random-id.ts +13 -0
  111. package/src/lib/show-submitted-data.tsx +14 -0
  112. package/src/lib/utils.test.ts +35 -0
  113. package/src/lib/utils.ts +78 -0
  114. package/src/secure-context.contract.test.ts +63 -0
  115. package/src/testids.contract.test.ts +113 -0
  116. package/src/testids.ts +8 -0
  117. 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,2 @@
1
+ /** Реэкспорт: список шрифтов платформо-независим и живёт в `@icomponents/tokens`. */
2
+ export * from "@icomponents/tokens/fonts";
@@ -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
+ };