@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,127 @@
1
+ import {
2
+ ChevronLeftIcon,
3
+ ChevronRightIcon,
4
+ DoubleArrowLeftIcon,
5
+ DoubleArrowRightIcon,
6
+ } from "@radix-ui/react-icons";
7
+ import { Button } from "@icomponents/react/components/ui/button";
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "@icomponents/react/components/ui/select";
15
+ import { cn, getPageNumbers } from "@icomponents/react/lib/utils";
16
+ import { type Table } from "@tanstack/react-table";
17
+
18
+ type DataTablePaginationProps<TData> = {
19
+ table: Table<TData>;
20
+ className?: string;
21
+ };
22
+
23
+ export function DataTablePagination<TData>({ table, className }: DataTablePaginationProps<TData>) {
24
+ const currentPage = table.getState().pagination.pageIndex + 1;
25
+ const totalPages = table.getPageCount();
26
+ const pageNumbers = getPageNumbers(currentPage, totalPages);
27
+
28
+ return (
29
+ <div
30
+ className={cn(
31
+ "flex items-center justify-between overflow-clip px-2",
32
+ "@max-2xl/content:flex-col-reverse @max-2xl/content:gap-4",
33
+ className
34
+ )}
35
+ style={{ overflowClipMargin: 1 }}
36
+ >
37
+ <div className="flex w-full items-center justify-between">
38
+ <div className="flex w-25 items-center justify-center text-sm font-medium @2xl/content:hidden">
39
+ Page {currentPage} of {totalPages}
40
+ </div>
41
+ <div className="flex items-center gap-2 @max-2xl/content:flex-row-reverse">
42
+ <Select
43
+ value={`${table.getState().pagination.pageSize}`}
44
+ onValueChange={(value) => {
45
+ table.setPageSize(Number(value));
46
+ }}
47
+ >
48
+ <SelectTrigger className="h-8 w-17.5">
49
+ <SelectValue placeholder={table.getState().pagination.pageSize} />
50
+ </SelectTrigger>
51
+ <SelectContent side="top">
52
+ {[10, 20, 30, 40, 50].map((pageSize) => (
53
+ <SelectItem key={pageSize} value={`${pageSize}`}>
54
+ {pageSize}
55
+ </SelectItem>
56
+ ))}
57
+ </SelectContent>
58
+ </Select>
59
+ <p className="hidden text-sm font-medium sm:block">Rows per page</p>
60
+ </div>
61
+ </div>
62
+
63
+ <div className="flex items-center sm:space-x-6 lg:space-x-8">
64
+ <div className="flex w-25 items-center justify-center text-sm font-medium @max-3xl/content:hidden">
65
+ Page {currentPage} of {totalPages}
66
+ </div>
67
+ <div className="flex items-center space-x-2">
68
+ <Button
69
+ variant="outline"
70
+ className="size-8 p-0 @max-md/content:hidden"
71
+ onClick={() => table.setPageIndex(0)}
72
+ disabled={!table.getCanPreviousPage()}
73
+ >
74
+ <span className="sr-only">Go to first page</span>
75
+ <DoubleArrowLeftIcon className="h-4 w-4" />
76
+ </Button>
77
+ <Button
78
+ variant="outline"
79
+ className="size-8 p-0"
80
+ onClick={() => table.previousPage()}
81
+ disabled={!table.getCanPreviousPage()}
82
+ >
83
+ <span className="sr-only">Go to previous page</span>
84
+ <ChevronLeftIcon className="h-4 w-4" />
85
+ </Button>
86
+
87
+ {/* Page number buttons */}
88
+ {pageNumbers.map((pageNumber, index) => (
89
+ <div key={`${pageNumber}-${index}`} className="flex items-center">
90
+ {pageNumber === "..." ? (
91
+ <span className="px-1 text-sm text-muted-foreground">...</span>
92
+ ) : (
93
+ <Button
94
+ variant={currentPage === pageNumber ? "default" : "outline"}
95
+ className="h-8 min-w-8 px-2"
96
+ onClick={() => table.setPageIndex((pageNumber as number) - 1)}
97
+ >
98
+ <span className="sr-only">Go to page {pageNumber}</span>
99
+ {pageNumber}
100
+ </Button>
101
+ )}
102
+ </div>
103
+ ))}
104
+
105
+ <Button
106
+ variant="outline"
107
+ className="size-8 p-0"
108
+ onClick={() => table.nextPage()}
109
+ disabled={!table.getCanNextPage()}
110
+ >
111
+ <span className="sr-only">Go to next page</span>
112
+ <ChevronRightIcon className="h-4 w-4" />
113
+ </Button>
114
+ <Button
115
+ variant="outline"
116
+ className="size-8 p-0 @max-md/content:hidden"
117
+ onClick={() => table.setPageIndex(table.getPageCount() - 1)}
118
+ disabled={!table.getCanNextPage()}
119
+ >
120
+ <span className="sr-only">Go to last page</span>
121
+ <DoubleArrowRightIcon className="h-4 w-4" />
122
+ </Button>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,12 @@
1
+ import "@tanstack/react-table";
2
+
3
+ // Module augmentation for the shared data-table pattern (mirrors the per-slice copies in
4
+ // packages/iam/src/features/*/tanstack-table.d.ts). Identical augmentations merge without conflict.
5
+ declare module "@tanstack/react-table" {
6
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
+ interface ColumnMeta<TData, TValue> {
8
+ className?: string; // apply to both th and td
9
+ tdClassName?: string;
10
+ thClassName?: string;
11
+ }
12
+ }
@@ -0,0 +1,88 @@
1
+ import { Cross2Icon } from "@radix-ui/react-icons";
2
+ import { Button } from "@icomponents/react/components/ui/button";
3
+ import { Input } from "@icomponents/react/components/ui/input";
4
+ import { type Table } from "@tanstack/react-table";
5
+
6
+ import { DataTableFacetedFilter } from "./faceted-filter";
7
+ import { DataTableViewOptions } from "./view-options";
8
+
9
+ type DataTableToolbarProps<TData> = {
10
+ table: Table<TData>;
11
+ searchPlaceholder?: string;
12
+ searchKey?: string;
13
+ filters?: {
14
+ columnId: string;
15
+ title: string;
16
+ options: {
17
+ label: string;
18
+ value: string;
19
+ icon?: React.ComponentType<{ className?: string }>;
20
+ }[];
21
+ testId?: string;
22
+ single?: boolean;
23
+ }[];
24
+ extra?: React.ReactNode;
25
+ };
26
+
27
+ export function DataTableToolbar<TData>({
28
+ table,
29
+ searchPlaceholder = "Filter...",
30
+ searchKey,
31
+ filters = [],
32
+ extra,
33
+ }: DataTableToolbarProps<TData>) {
34
+ const isFiltered = table.getState().columnFilters.length > 0 || table.getState().globalFilter;
35
+
36
+ return (
37
+ <div className="flex items-center justify-between">
38
+ <div className="flex flex-1 flex-col-reverse items-start gap-y-2 sm:flex-row sm:items-center sm:space-x-2">
39
+ {searchKey ? (
40
+ <Input
41
+ placeholder={searchPlaceholder}
42
+ value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ""}
43
+ onChange={(event) => table.getColumn(searchKey)?.setFilterValue(event.target.value)}
44
+ className="h-8 w-37.5 lg:w-62.5"
45
+ />
46
+ ) : (
47
+ <Input
48
+ placeholder={searchPlaceholder}
49
+ value={table.getState().globalFilter ?? ""}
50
+ onChange={(event) => table.setGlobalFilter(event.target.value)}
51
+ className="h-8 w-37.5 lg:w-62.5"
52
+ />
53
+ )}
54
+ <div className="flex gap-x-2">
55
+ {filters.map((filter) => {
56
+ const column = table.getColumn(filter.columnId);
57
+ if (!column) return null;
58
+ return (
59
+ <DataTableFacetedFilter
60
+ key={filter.columnId}
61
+ column={column}
62
+ title={filter.title}
63
+ options={filter.options}
64
+ testId={filter.testId}
65
+ single={filter.single}
66
+ />
67
+ );
68
+ })}
69
+ </div>
70
+ {extra}
71
+ {isFiltered && (
72
+ <Button
73
+ variant="ghost"
74
+ onClick={() => {
75
+ table.resetColumnFilters();
76
+ table.setGlobalFilter("");
77
+ }}
78
+ className="h-8 px-2 lg:px-3"
79
+ >
80
+ Reset
81
+ <Cross2Icon className="ms-2 h-4 w-4" />
82
+ </Button>
83
+ )}
84
+ </div>
85
+ <DataTableViewOptions table={table} />
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,49 @@
1
+ import { MixerHorizontalIcon } from "@radix-ui/react-icons";
2
+ import { Button } from "@icomponents/react/components/ui/button";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuCheckboxItem,
6
+ DropdownMenuContent,
7
+ DropdownMenuLabel,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuTrigger,
10
+ } from "@icomponents/react/components/ui/dropdown-menu";
11
+ import { type Table } from "@tanstack/react-table";
12
+
13
+ type DataTableViewOptionsProps<TData> = {
14
+ table: Table<TData>;
15
+ };
16
+
17
+ export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps<TData>) {
18
+ return (
19
+ <DropdownMenu modal={false}>
20
+ <DropdownMenuTrigger
21
+ render={
22
+ <Button variant="outline" size="sm" className="ms-auto hidden h-8 lg:flex">
23
+ <MixerHorizontalIcon className="size-4" />
24
+ View
25
+ </Button>
26
+ }
27
+ />
28
+ <DropdownMenuContent align="end" className="w-37.5">
29
+ <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
30
+ <DropdownMenuSeparator />
31
+ {table
32
+ .getAllColumns()
33
+ .filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
34
+ .map((column) => {
35
+ return (
36
+ <DropdownMenuCheckboxItem
37
+ key={column.id}
38
+ className="capitalize"
39
+ checked={column.getIsVisible()}
40
+ onCheckedChange={(value) => column.toggleVisibility(!!value)}
41
+ >
42
+ {column.id}
43
+ </DropdownMenuCheckboxItem>
44
+ );
45
+ })}
46
+ </DropdownMenuContent>
47
+ </DropdownMenu>
48
+ );
49
+ }
@@ -0,0 +1,39 @@
1
+ import { Button } from "@icomponents/react/components/ui/button";
2
+ import { Calendar } from "@icomponents/react/components/ui/calendar";
3
+ import { Popover, PopoverContent, PopoverTrigger } from "@icomponents/react/components/ui/popover";
4
+ import { format } from "date-fns";
5
+ import { Calendar as CalendarIcon } from "lucide-react";
6
+
7
+ type DatePickerProps = {
8
+ selected: Date | undefined;
9
+ onSelect: (date: Date | undefined) => void;
10
+ placeholder?: string;
11
+ };
12
+
13
+ export function DatePicker({ selected, onSelect, placeholder = "Pick a date" }: DatePickerProps) {
14
+ return (
15
+ <Popover>
16
+ <PopoverTrigger
17
+ render={
18
+ <Button
19
+ variant="outline"
20
+ data-empty={!selected}
21
+ className="w-60 justify-start text-start font-normal data-[empty=true]:text-muted-foreground"
22
+ >
23
+ {selected ? format(selected, "MMM d, yyyy") : <span>{placeholder}</span>}
24
+ <CalendarIcon className="ms-auto h-4 w-4 opacity-50" />
25
+ </Button>
26
+ }
27
+ />
28
+ <PopoverContent className="w-auto p-0">
29
+ <Calendar
30
+ mode="single"
31
+ captionLayout="dropdown"
32
+ selected={selected}
33
+ onSelect={onSelect}
34
+ disabled={(date: Date) => date > new Date() || date < new Date("1900-01-01")}
35
+ />
36
+ </PopoverContent>
37
+ </Popover>
38
+ );
39
+ }
@@ -0,0 +1,63 @@
1
+ import { cn } from "@icomponents/react/lib/utils";
2
+ import * as React from "react";
3
+
4
+ // Section card in the Evoca design language: a white surface with a hairline border, 4px radius and
5
+ // a soft layered shadow, opened by a fixed-height header row (condensed display title + optional
6
+ // right-aligned action). Reused across company detail / registry / dashboard so every panel reads
7
+ // as one consistent, richer card system (vs a flat bordered box).
8
+ export function DetailCard({
9
+ title,
10
+ action,
11
+ bodyClassName,
12
+ className,
13
+ children,
14
+ ...props
15
+ }: React.ComponentProps<"section"> & {
16
+ title: React.ReactNode;
17
+ action?: React.ReactNode;
18
+ bodyClassName?: string;
19
+ }) {
20
+ return (
21
+ <section
22
+ data-component="detail-card"
23
+ className={cn("overflow-hidden rounded border border-border bg-card soft-shadow", className)}
24
+ {...props}
25
+ >
26
+ <div className="flex h-14 items-center justify-between gap-3 border-b border-border px-5">
27
+ <h2 className="font-display text-[20px] font-bold leading-none tracking-tight text-foreground">
28
+ {title}
29
+ </h2>
30
+ {action}
31
+ </div>
32
+ <div className={bodyClassName}>{children}</div>
33
+ </section>
34
+ );
35
+ }
36
+
37
+ // One definition-list row inside a DetailCard body (wrap rows in a <dl className="divide-y
38
+ // divide-border">). Label is muted + fixed-width; value is foreground, monospaced+tabular for
39
+ // numeric/identifier data.
40
+ export function DetailRow({
41
+ label,
42
+ value,
43
+ mono,
44
+ }: {
45
+ label: React.ReactNode;
46
+ value: React.ReactNode;
47
+ mono?: boolean;
48
+ }) {
49
+ return (
50
+ <div className="flex items-start gap-4 px-5 py-3">
51
+ <dt className="w-44 shrink-0 text-[13px] text-muted-foreground">{label}</dt>
52
+ <dd
53
+ className={
54
+ mono
55
+ ? "font-mono text-[13.5px] tabular-nums text-foreground"
56
+ : "text-[14px] font-medium text-foreground"
57
+ }
58
+ >
59
+ {value}
60
+ </dd>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,38 @@
1
+ import { Button } from "@icomponents/react/components/ui/button";
2
+ import {
3
+ DropdownMenu,
4
+ DropdownMenuContent,
5
+ DropdownMenuItem,
6
+ DropdownMenuTrigger,
7
+ } from "@icomponents/react/components/ui/dropdown-menu";
8
+ import { useLanguage } from "@icomponents/react/context/language-provider";
9
+ import type { Locale } from "@icomponents/react/i18n/types";
10
+ import { cn } from "@icomponents/react/lib/utils";
11
+ import { Check, Languages } from "lucide-react";
12
+
13
+ const LABEL: Record<Locale, string> = { ru: "Русский", en: "English" };
14
+
15
+ /** Компактный переключатель языка для шапки (рядом с ThemeSwitch). Меняет i18next + куку + <html lang>. */
16
+ export function LanguageSwitch() {
17
+ const { lang, langs, setLang } = useLanguage();
18
+ return (
19
+ <DropdownMenu modal={false}>
20
+ <DropdownMenuTrigger
21
+ render={
22
+ <Button variant="ghost" size="icon" className="scale-95 rounded-full">
23
+ <Languages className="size-[1.2rem]" />
24
+ <span className="sr-only">Сменить язык / Switch language</span>
25
+ </Button>
26
+ }
27
+ />
28
+ <DropdownMenuContent align="end">
29
+ {langs.map((l) => (
30
+ <DropdownMenuItem key={l} onClick={() => setLang(l)}>
31
+ {LABEL[l]}
32
+ <Check size={14} className={cn("ms-auto", lang !== l && "hidden")} />
33
+ </DropdownMenuItem>
34
+ ))}
35
+ </DropdownMenuContent>
36
+ </DropdownMenu>
37
+ );
38
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ Sidebar,
3
+ SidebarContent,
4
+ SidebarFooter,
5
+ SidebarHeader,
6
+ SidebarRail,
7
+ } from "@icomponents/react/components/ui/sidebar";
8
+ import { useLayout } from "@icomponents/react/context/layout-provider";
9
+ import { TESTID, testid } from "@icomponents/react/testids";
10
+
11
+ // import { AppTitle } from './app-title'
12
+ import { NavGroup } from "./nav-group";
13
+ import { NavUser, type NavUserItem } from "./nav-user";
14
+ import { TeamSwitcher } from "./team-switcher";
15
+ import { type NavGroup as NavGroupType } from "./types";
16
+
17
+ type Team = {
18
+ name: string;
19
+ logo: React.ElementType;
20
+ plan: string;
21
+ };
22
+
23
+ type AppSidebarProps = {
24
+ user: {
25
+ name: string;
26
+ email: string;
27
+ avatar?: string;
28
+ };
29
+ navGroups: NavGroupType[];
30
+ teams: Team[];
31
+ navUserItems: NavUserItem[];
32
+ onSignOut: () => void;
33
+ };
34
+
35
+ export function AppSidebar({ user, navGroups, teams, navUserItems, onSignOut }: AppSidebarProps) {
36
+ const { collapsible, variant } = useLayout();
37
+
38
+ return (
39
+ <Sidebar collapsible={collapsible} variant={variant}>
40
+ <SidebarHeader>
41
+ <TeamSwitcher teams={teams} />
42
+
43
+ {/* Replace <TeamSwitch /> with the following <AppTitle />
44
+ /* if you want to use the normal app title instead of TeamSwitch dropdown */}
45
+ {/* <AppTitle /> */}
46
+ </SidebarHeader>
47
+ {/* Пропала навигация — человек заперт на одном экране, и это не «косметика» (см. testids.ts). */}
48
+ <SidebarContent {...testid(TESTID.nav)}>
49
+ {navGroups.map((props) => (
50
+ <NavGroup key={props.title} {...props} />
51
+ ))}
52
+ </SidebarContent>
53
+ <SidebarFooter>
54
+ <NavUser user={user} items={navUserItems} onSignOut={onSignOut} />
55
+ </SidebarFooter>
56
+ <SidebarRail />
57
+ </Sidebar>
58
+ );
59
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ SidebarMenu,
3
+ SidebarMenuButton,
4
+ SidebarMenuItem,
5
+ useSidebar,
6
+ } from "@icomponents/react/components/ui/sidebar";
7
+ import { cn } from "@icomponents/react/lib/utils";
8
+ import { Link } from "@tanstack/react-router";
9
+ import { Menu, X } from "lucide-react";
10
+
11
+ import { Button } from "../ui/button";
12
+
13
+ // `appName` mirrors config.appName (default "Admin Console") — threaded as a prop since this is a
14
+ // client component with no server-config seam; callers that have the runtime config can pass it.
15
+ export function AppTitle({ appName = "Admin Console" }: { appName?: string } = {}) {
16
+ const { setOpenMobile } = useSidebar();
17
+ return (
18
+ <SidebarMenu>
19
+ <SidebarMenuItem>
20
+ <SidebarMenuButton
21
+ size="lg"
22
+ className="gap-0 py-0 hover:bg-transparent active:bg-transparent"
23
+ asChild
24
+ >
25
+ <div>
26
+ <Link
27
+ to="/"
28
+ onClick={() => setOpenMobile(false)}
29
+ className="grid flex-1 text-start text-sm leading-tight"
30
+ >
31
+ <span className="truncate font-bold">{appName}</span>
32
+ </Link>
33
+ <ToggleSidebar />
34
+ </div>
35
+ </SidebarMenuButton>
36
+ </SidebarMenuItem>
37
+ </SidebarMenu>
38
+ );
39
+ }
40
+
41
+ function ToggleSidebar({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
42
+ const { toggleSidebar } = useSidebar();
43
+
44
+ return (
45
+ <Button
46
+ data-sidebar="trigger"
47
+ data-slot="sidebar-trigger"
48
+ variant="ghost"
49
+ size="icon"
50
+ className={cn("aspect-square size-8 max-md:scale-125", className)}
51
+ onClick={(event) => {
52
+ onClick?.(event);
53
+ toggleSidebar();
54
+ }}
55
+ {...props}
56
+ >
57
+ <X className="md:hidden" />
58
+ <Menu className="max-md:hidden" />
59
+ <span className="sr-only">Toggle Sidebar</span>
60
+ </Button>
61
+ );
62
+ }
@@ -0,0 +1,61 @@
1
+ import { Separator } from "@icomponents/react/components/ui/separator";
2
+ import { SidebarTrigger } from "@icomponents/react/components/ui/sidebar";
3
+ import { cn } from "@icomponents/react/lib/utils";
4
+ import { useEffect, useState } from "react";
5
+
6
+ type HeaderProps = React.HTMLAttributes<HTMLElement> & {
7
+ fixed?: boolean;
8
+ /**
9
+ * Suppress the Header's own SidebarTrigger (+ its leading separator). The global
10
+ * SidebarRail already provides a "Toggle Sidebar" affordance on every authenticated
11
+ * page, so a page that would otherwise show two toggles (e.g. /company-audit) opts out
12
+ * here. Default false keeps every other Header page unchanged (AC-F4-5, narrow fix).
13
+ */
14
+ hideSidebarTrigger?: boolean;
15
+ ref?: React.Ref<HTMLElement>;
16
+ };
17
+
18
+ export function Header({ className, fixed, hideSidebarTrigger, children, ...props }: HeaderProps) {
19
+ const [offset, setOffset] = useState(0);
20
+
21
+ useEffect(() => {
22
+ const onScroll = () => {
23
+ setOffset(document.body.scrollTop || document.documentElement.scrollTop);
24
+ };
25
+
26
+ // Add scroll listener to the body
27
+ document.addEventListener("scroll", onScroll, { passive: true });
28
+
29
+ // Clean up the event listener on unmount
30
+ return () => document.removeEventListener("scroll", onScroll);
31
+ }, []);
32
+
33
+ return (
34
+ <header
35
+ className={cn(
36
+ "z-50 h-16",
37
+ fixed && "header-fixed peer/header sticky top-0 w-[inherit]",
38
+ offset > 10 && fixed ? "shadow" : "shadow-none",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ <div
44
+ className={cn(
45
+ "relative flex h-full items-center gap-3 p-4 sm:gap-4",
46
+ offset > 10 &&
47
+ fixed &&
48
+ "after:absolute after:inset-0 after:-z-10 after:bg-background/20 after:backdrop-blur-lg"
49
+ )}
50
+ >
51
+ {!hideSidebarTrigger && (
52
+ <>
53
+ <SidebarTrigger variant="outline" className="max-md:scale-125" />
54
+ <Separator orientation="vertical" className="h-6" />
55
+ </>
56
+ )}
57
+ {children}
58
+ </div>
59
+ </header>
60
+ );
61
+ }
@@ -0,0 +1,34 @@
1
+ import { cn } from "@icomponents/react/lib/utils";
2
+ import { TESTID, testid } from "@icomponents/react/testids";
3
+
4
+ type MainProps = React.HTMLAttributes<HTMLElement> & {
5
+ fixed?: boolean;
6
+ fluid?: boolean;
7
+ ref?: React.Ref<HTMLElement>;
8
+ };
9
+
10
+ /**
11
+ * Контейнер содержимого КАЖДОГО экрана. Несёт `data-testid="screen"` — это и есть признак «экран
12
+ * смонтировался», по которому e2e отличает живой экран от каркаса и от страницы ошибки. Вешаем здесь,
13
+ * а не в шапке страницы: шапку экран может и не использовать (дашборд не использует), а контейнер —
14
+ * использует всегда.
15
+ */
16
+ export function Main({ fixed, className, fluid, ...props }: MainProps) {
17
+ return (
18
+ <main
19
+ {...testid(TESTID.screen)}
20
+ data-layout={fixed ? "fixed" : "auto"}
21
+ className={cn(
22
+ "px-4 py-6",
23
+
24
+ // If layout is fixed, make the main container flex and grow
25
+ fixed && "flex grow flex-col overflow-hidden",
26
+
27
+ // If layout is not fluid, set the max-width
28
+ !fluid && "@7xl/content:mx-auto @7xl/content:w-full @7xl/content:max-w-7xl",
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ );
34
+ }