@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,185 @@
1
+ import {
2
+ Collapsible,
3
+ CollapsibleContent,
4
+ CollapsibleTrigger,
5
+ } from "@icomponents/react/components/ui/collapsible";
6
+ import {
7
+ SidebarGroup,
8
+ SidebarGroupLabel,
9
+ SidebarMenu,
10
+ SidebarMenuButton,
11
+ SidebarMenuItem,
12
+ SidebarMenuSub,
13
+ SidebarMenuSubButton,
14
+ SidebarMenuSubItem,
15
+ useSidebar,
16
+ } from "@icomponents/react/components/ui/sidebar";
17
+ import { Link, useLocation } from "@tanstack/react-router";
18
+ import { ChevronRight } from "lucide-react";
19
+ import { type ReactNode } from "react";
20
+
21
+ import { TESTID, testid } from "../../testids";
22
+ import { Badge } from "../ui/badge";
23
+ import {
24
+ DropdownMenu,
25
+ DropdownMenuContent,
26
+ DropdownMenuItem,
27
+ DropdownMenuLabel,
28
+ DropdownMenuSeparator,
29
+ DropdownMenuTrigger,
30
+ } from "../ui/dropdown-menu";
31
+ import {
32
+ type NavCollapsible,
33
+ type NavItem,
34
+ type NavLink,
35
+ type NavGroup as NavGroupProps,
36
+ } from "./types";
37
+
38
+ /**
39
+ * Устойчивая идентичность пункта меню — его АДРЕС, а не подпись.
40
+ *
41
+ * Подпись пункта — это перевод: «Companies» и «Компании» — один и тот же пункт, и e2e, написанный
42
+ * по подписи, краснеет от смены локали или редизайна, ничего не доказав (ровно так протух
43
+ * `platform.e2e.ts`). Адрес же не меняется ни от перевода, ни от вёрстки — и он же отвечает на
44
+ * вопрос, который тест на самом деле задаёт: «виден ли пользователю путь в этот раздел».
45
+ *
46
+ * `/organization-users` → `nav-item-organization-users`, `/` → `nav-item-root`.
47
+ */
48
+ const navKey = (url: string | undefined): string =>
49
+ (url ?? "").replace(/^\/+/, "").replaceAll("/", "-") || "root";
50
+
51
+ export function NavGroup({ title, items }: NavGroupProps) {
52
+ const { state, isMobile } = useSidebar();
53
+ const href = useLocation({ select: (location) => location.href });
54
+ return (
55
+ <SidebarGroup>
56
+ <SidebarGroupLabel>{title}</SidebarGroupLabel>
57
+ <SidebarMenu>
58
+ {items.map((item) => {
59
+ const key = `${item.title}-${item.url}`;
60
+
61
+ if (!item.items) return <SidebarMenuLink key={key} item={item} href={href} />;
62
+
63
+ if (state === "collapsed" && !isMobile)
64
+ return <SidebarMenuCollapsedDropdown key={key} item={item} href={href} />;
65
+
66
+ return <SidebarMenuCollapsible key={key} item={item} href={href} />;
67
+ })}
68
+ </SidebarMenu>
69
+ </SidebarGroup>
70
+ );
71
+ }
72
+
73
+ function NavBadge({ children }: { children: ReactNode }) {
74
+ return <Badge className="rounded-full px-1 py-0 text-xs">{children}</Badge>;
75
+ }
76
+
77
+ function SidebarMenuLink({ item, href }: { item: NavLink; href: string }) {
78
+ const { setOpenMobile } = useSidebar();
79
+ return (
80
+ <SidebarMenuItem>
81
+ <SidebarMenuButton asChild isActive={checkIsActive(href, item)} tooltip={item.title}>
82
+ <Link
83
+ to={item.url}
84
+ {...testid(TESTID.navItem(navKey(item.url)))}
85
+ onClick={() => setOpenMobile(false)}
86
+ >
87
+ {item.icon && <item.icon />}
88
+ <span>{item.title}</span>
89
+ {item.badge && <NavBadge>{item.badge}</NavBadge>}
90
+ </Link>
91
+ </SidebarMenuButton>
92
+ </SidebarMenuItem>
93
+ );
94
+ }
95
+
96
+ function SidebarMenuCollapsible({ item, href }: { item: NavCollapsible; href: string }) {
97
+ const { setOpenMobile } = useSidebar();
98
+ return (
99
+ <Collapsible
100
+ render={<SidebarMenuItem />}
101
+ defaultOpen={checkIsActive(href, item, true)}
102
+ className="group/collapsible"
103
+ >
104
+ <CollapsibleTrigger render={<SidebarMenuButton tooltip={item.title} />}>
105
+ {item.icon && <item.icon />}
106
+ <span>{item.title}</span>
107
+ {item.badge && <NavBadge>{item.badge}</NavBadge>}
108
+ <ChevronRight className="ms-auto transition-transform duration-200 group-data-open/collapsible:rotate-90 rtl:rotate-180" />
109
+ </CollapsibleTrigger>
110
+ <CollapsibleContent className="CollapsibleContent">
111
+ <SidebarMenuSub>
112
+ {item.items.map((subItem) => (
113
+ <SidebarMenuSubItem key={subItem.title}>
114
+ <SidebarMenuSubButton asChild isActive={checkIsActive(href, subItem)}>
115
+ <Link
116
+ to={subItem.url}
117
+ {...testid(TESTID.navItem(navKey(subItem.url)))}
118
+ onClick={() => setOpenMobile(false)}
119
+ >
120
+ {subItem.icon && <subItem.icon />}
121
+ <span>{subItem.title}</span>
122
+ {subItem.badge && <NavBadge>{subItem.badge}</NavBadge>}
123
+ </Link>
124
+ </SidebarMenuSubButton>
125
+ </SidebarMenuSubItem>
126
+ ))}
127
+ </SidebarMenuSub>
128
+ </CollapsibleContent>
129
+ </Collapsible>
130
+ );
131
+ }
132
+
133
+ function SidebarMenuCollapsedDropdown({ item, href }: { item: NavCollapsible; href: string }) {
134
+ return (
135
+ <SidebarMenuItem>
136
+ <DropdownMenu>
137
+ <DropdownMenuTrigger
138
+ render={
139
+ <SidebarMenuButton
140
+ tooltip={item.title}
141
+ isActive={checkIsActive(href, item)}
142
+ className="group/collapsed-dropdown"
143
+ >
144
+ {item.icon && <item.icon />}
145
+ <span>{item.title}</span>
146
+ {item.badge && <NavBadge>{item.badge}</NavBadge>}
147
+ <ChevronRight className="ms-auto transition-transform duration-200 group-data-popup-open/collapsed-dropdown:rotate-90" />
148
+ </SidebarMenuButton>
149
+ }
150
+ />
151
+ <DropdownMenuContent side="right" align="start" sideOffset={4}>
152
+ <DropdownMenuLabel>
153
+ {item.title} {item.badge ? `(${item.badge})` : ""}
154
+ </DropdownMenuLabel>
155
+ <DropdownMenuSeparator />
156
+ {item.items.map((sub) => (
157
+ <DropdownMenuItem
158
+ key={`${sub.title}-${sub.url}`}
159
+ render={
160
+ <Link
161
+ to={sub.url}
162
+ {...testid(TESTID.navItem(navKey(sub.url)))}
163
+ className={`${checkIsActive(href, sub) ? "bg-secondary" : ""}`}
164
+ >
165
+ {sub.icon && <sub.icon />}
166
+ <span className="max-w-52 text-wrap">{sub.title}</span>
167
+ {sub.badge && <span className="ms-auto text-xs">{sub.badge}</span>}
168
+ </Link>
169
+ }
170
+ />
171
+ ))}
172
+ </DropdownMenuContent>
173
+ </DropdownMenu>
174
+ </SidebarMenuItem>
175
+ );
176
+ }
177
+
178
+ function checkIsActive(href: string, item: NavItem, mainNav = false) {
179
+ return (
180
+ href === item.url || // /endpint?search=param
181
+ href.split("?")[0] === item.url || // endpoint
182
+ !!item?.items?.filter((i) => i.url === href).length || // if child nav is active
183
+ (mainNav && href.split("/")[1] !== "" && href.split("/")[1] === item?.url?.split("/")[1])
184
+ );
185
+ }
@@ -0,0 +1,115 @@
1
+ import { SignOutDialog } from "@icomponents/react/components/sign-out-dialog";
2
+ import { Avatar, AvatarFallback, AvatarImage } from "@icomponents/react/components/ui/avatar";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuGroup,
7
+ DropdownMenuItem,
8
+ DropdownMenuLabel,
9
+ DropdownMenuSeparator,
10
+ DropdownMenuTrigger,
11
+ } from "@icomponents/react/components/ui/dropdown-menu";
12
+ import {
13
+ SidebarMenu,
14
+ SidebarMenuButton,
15
+ SidebarMenuItem,
16
+ useSidebar,
17
+ } from "@icomponents/react/components/ui/sidebar";
18
+ import useDialogState from "@icomponents/react/hooks/use-dialog-state";
19
+ import { Link, type LinkProps } from "@tanstack/react-router";
20
+ import { ChevronsUpDown, LogOut } from "lucide-react";
21
+ import { type ElementType } from "react";
22
+
23
+ export type NavUserItem = {
24
+ title: string;
25
+ url: LinkProps["to"] | (string & {});
26
+ icon?: ElementType;
27
+ };
28
+
29
+ type NavUserProps = {
30
+ user: {
31
+ name: string;
32
+ email: string;
33
+ avatar?: string;
34
+ };
35
+ items: NavUserItem[];
36
+ onSignOut: () => void;
37
+ };
38
+
39
+ export function NavUser({ user, items, onSignOut }: NavUserProps) {
40
+ const { isMobile } = useSidebar();
41
+ const [open, setOpen] = useDialogState();
42
+
43
+ return (
44
+ <>
45
+ <SidebarMenu>
46
+ <SidebarMenuItem>
47
+ <DropdownMenu>
48
+ <DropdownMenuTrigger
49
+ render={
50
+ <SidebarMenuButton
51
+ size="lg"
52
+ className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground"
53
+ >
54
+ <Avatar className="h-8 w-8 rounded-lg">
55
+ <AvatarImage src={user.avatar} alt={user.name} />
56
+ <AvatarFallback className="rounded-lg">SN</AvatarFallback>
57
+ </Avatar>
58
+ <div className="grid flex-1 text-start text-sm leading-tight">
59
+ <span className="truncate font-semibold">{user.name}</span>
60
+ <span className="truncate text-xs">{user.email}</span>
61
+ </div>
62
+ <ChevronsUpDown className="ms-auto size-4" />
63
+ </SidebarMenuButton>
64
+ }
65
+ />
66
+ <DropdownMenuContent
67
+ className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
68
+ side={isMobile ? "bottom" : "right"}
69
+ align="end"
70
+ sideOffset={4}
71
+ >
72
+ <DropdownMenuLabel className="p-0 font-normal">
73
+ <div className="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
74
+ <Avatar className="h-8 w-8 rounded-lg">
75
+ <AvatarImage src={user.avatar} alt={user.name} />
76
+ <AvatarFallback className="rounded-lg">SN</AvatarFallback>
77
+ </Avatar>
78
+ <div className="grid flex-1 text-start text-sm leading-tight">
79
+ <span className="truncate font-semibold">{user.name}</span>
80
+ <span className="truncate text-xs">{user.email}</span>
81
+ </div>
82
+ </div>
83
+ </DropdownMenuLabel>
84
+ <DropdownMenuSeparator />
85
+ <DropdownMenuGroup>
86
+ {items.map((item) => (
87
+ <DropdownMenuItem
88
+ key={item.title}
89
+ render={
90
+ <Link to={item.url}>
91
+ {item.icon && <item.icon />}
92
+ {item.title}
93
+ </Link>
94
+ }
95
+ />
96
+ ))}
97
+ </DropdownMenuGroup>
98
+ <DropdownMenuSeparator />
99
+ <DropdownMenuItem
100
+ data-testid="sign-out"
101
+ variant="destructive"
102
+ onClick={() => setOpen(true)}
103
+ >
104
+ <LogOut />
105
+ Sign out
106
+ </DropdownMenuItem>
107
+ </DropdownMenuContent>
108
+ </DropdownMenu>
109
+ </SidebarMenuItem>
110
+ </SidebarMenu>
111
+
112
+ <SignOutDialog open={!!open} onOpenChange={setOpen} onSignOut={onSignOut} />
113
+ </>
114
+ );
115
+ }
@@ -0,0 +1,119 @@
1
+ import { cn } from "@icomponents/react/lib/utils";
2
+ import { TESTID, testid } from "@icomponents/react/testids";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import * as React from "react";
5
+
6
+ /**
7
+ * Шапка экрана — одна на всю консоль.
8
+ *
9
+ * Все экраны прототипа (`prototype/*.html`) несут `data-component="page-header"` ОДНОЙ структуры:
10
+ * моно-kicker с коротким штрихом бренда → `h1` display-шрифтом → абзац-объяснение → капсулы-бейджи,
11
+ * а справа — пара со-первичных действий. Это замер по семи экранам, а не догадка, поэтому шапка
12
+ * живёт компонентом, а не копией в каждом экране.
13
+ *
14
+ * Почему это важнее экономии строк: здесь навешен `TESTID.screenTitle`. Пока заголовок собирал
15
+ * каждый экран сам, контракт вёрстки держался на памяти автора — а забытый `data-testid` не роняет
16
+ * ни линтер, ни тайпчек, он тихо выключает e2e, который этот экран проверял.
17
+ *
18
+ * `description` и `actions` — `ReactNode`: в описании стоят моноширинные технические литералы
19
+ * (`draft → sandbox → active`, `409`), которые НЕ переводятся и собираются вызывающим экраном.
20
+ */
21
+ const screenBadgeVariants = cva(
22
+ "inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-[12px] whitespace-nowrap",
23
+ {
24
+ variants: {
25
+ tone: {
26
+ primary: "border-primary/40 bg-primary/10 font-medium text-primary",
27
+ neutral: "border-border bg-card text-muted-foreground",
28
+ success: "border-success/40 bg-success/10 font-semibold text-success",
29
+ destructive: "border-destructive/40 bg-destructive/10 font-semibold text-destructive",
30
+ },
31
+ },
32
+ defaultVariants: { tone: "neutral" },
33
+ }
34
+ );
35
+
36
+ export type ScreenBadgeTone = NonNullable<VariantProps<typeof screenBadgeVariants>["tone"]>;
37
+
38
+ /**
39
+ * Капсула под заголовком: область действия права, режим, состояние стенда.
40
+ *
41
+ * Иконка типизирована структурно (`ComponentType<{ className }>`), а не импортом из набора: ядро
42
+ * не должно тащить зависимость на конкретную библиотеку иконок ради подписи в шапке.
43
+ */
44
+ export function ScreenBadge({
45
+ tone,
46
+ icon: Icon,
47
+ className,
48
+ children,
49
+ ...props
50
+ }: React.ComponentProps<"span"> &
51
+ VariantProps<typeof screenBadgeVariants> & {
52
+ icon?: React.ComponentType<{ className?: string }>;
53
+ }) {
54
+ return (
55
+ <span className={cn(screenBadgeVariants({ tone }), className)} {...props}>
56
+ {Icon ? <Icon className="size-3.5 shrink-0" /> : null}
57
+ {children}
58
+ </span>
59
+ );
60
+ }
61
+
62
+ type ScreenHeaderProps = {
63
+ /** Моно-капс над заголовком (`platform · control plane`). Латиница-термин — НЕ переводится. */
64
+ kicker?: string;
65
+ /** `h1` экрана. Несёт `TESTID.screenTitle` — отдельно вешать не нужно и не надо. */
66
+ title: React.ReactNode;
67
+ /** Абзац-объяснение. `ReactNode`, потому что внутри моноширинные литералы. */
68
+ description?: React.ReactNode;
69
+ /** Ряд капсул (`ScreenBadge`). */
70
+ badges?: React.ReactNode;
71
+ /** Со-первичные действия справа. */
72
+ actions?: React.ReactNode;
73
+ className?: string;
74
+ };
75
+
76
+ export function ScreenHeader({
77
+ kicker,
78
+ title,
79
+ description,
80
+ badges,
81
+ actions,
82
+ className,
83
+ }: ScreenHeaderProps) {
84
+ return (
85
+ <section
86
+ className={cn("flex flex-wrap items-end justify-between gap-x-6 gap-y-4", className)}
87
+ data-slot="screen-header"
88
+ >
89
+ <div className="min-w-0">
90
+ {kicker ? (
91
+ <div className="flex items-center gap-2 font-mono text-[11px] font-medium tracking-[0.14em] text-muted-foreground uppercase">
92
+ <span className="h-px w-6 bg-primary" aria-hidden />
93
+ {kicker}
94
+ </div>
95
+ ) : null}
96
+
97
+ <h1
98
+ {...testid(TESTID.screenTitle)}
99
+ className={cn(
100
+ "font-display text-[32px] leading-[1.1] font-bold tracking-tight",
101
+ kicker && "mt-1.5"
102
+ )}
103
+ >
104
+ {title}
105
+ </h1>
106
+
107
+ {description ? (
108
+ <p className="mt-2 max-w-[64ch] text-[13px] leading-snug text-muted-foreground">
109
+ {description}
110
+ </p>
111
+ ) : null}
112
+
113
+ {badges ? <div className="mt-3 flex flex-wrap items-center gap-2">{badges}</div> : null}
114
+ </div>
115
+
116
+ {actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
117
+ </section>
118
+ );
119
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ DropdownMenu,
3
+ DropdownMenuContent,
4
+ DropdownMenuItem,
5
+ DropdownMenuLabel,
6
+ DropdownMenuSeparator,
7
+ DropdownMenuShortcut,
8
+ DropdownMenuTrigger,
9
+ } from "@icomponents/react/components/ui/dropdown-menu";
10
+ import {
11
+ SidebarMenu,
12
+ SidebarMenuButton,
13
+ SidebarMenuItem,
14
+ useSidebar,
15
+ } from "@icomponents/react/components/ui/sidebar";
16
+ import { ChevronsUpDown, Plus } from "lucide-react";
17
+ import * as React from "react";
18
+
19
+ type TeamSwitcherProps = {
20
+ teams: {
21
+ name: string;
22
+ logo: React.ElementType;
23
+ plan: string;
24
+ }[];
25
+ };
26
+
27
+ export function TeamSwitcher({ teams }: TeamSwitcherProps) {
28
+ const { isMobile } = useSidebar();
29
+ const [activeTeam, setActiveTeam] = React.useState(teams[0]);
30
+
31
+ return (
32
+ <SidebarMenu>
33
+ <SidebarMenuItem>
34
+ <DropdownMenu>
35
+ <DropdownMenuTrigger
36
+ render={
37
+ <SidebarMenuButton
38
+ size="lg"
39
+ className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground"
40
+ >
41
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
42
+ <activeTeam.logo className="size-4" />
43
+ </div>
44
+ <div className="grid flex-1 text-start text-sm leading-tight">
45
+ <span className="truncate font-semibold">{activeTeam.name}</span>
46
+ <span className="truncate text-xs">{activeTeam.plan}</span>
47
+ </div>
48
+ <ChevronsUpDown className="ms-auto" />
49
+ </SidebarMenuButton>
50
+ }
51
+ />
52
+ <DropdownMenuContent
53
+ className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
54
+ align="start"
55
+ side={isMobile ? "bottom" : "right"}
56
+ sideOffset={4}
57
+ >
58
+ <DropdownMenuLabel className="text-xs text-muted-foreground">Teams</DropdownMenuLabel>
59
+ {teams.map((team, index) => (
60
+ <DropdownMenuItem
61
+ key={team.name}
62
+ onClick={() => setActiveTeam(team)}
63
+ className="gap-2 p-2"
64
+ >
65
+ <div className="flex size-6 items-center justify-center rounded-sm border">
66
+ <team.logo className="size-4 shrink-0" />
67
+ </div>
68
+ {team.name}
69
+ <DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
70
+ </DropdownMenuItem>
71
+ ))}
72
+ <DropdownMenuSeparator />
73
+ <DropdownMenuItem className="gap-2 p-2">
74
+ <div className="flex size-6 items-center justify-center rounded-md border bg-background">
75
+ <Plus className="size-4" />
76
+ </div>
77
+ <div className="font-medium text-muted-foreground">Add team</div>
78
+ </DropdownMenuItem>
79
+ </DropdownMenuContent>
80
+ </DropdownMenu>
81
+ </SidebarMenuItem>
82
+ </SidebarMenu>
83
+ );
84
+ }
@@ -0,0 +1,68 @@
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 { cn } from "@icomponents/react/lib/utils";
9
+ import { Link } from "@tanstack/react-router";
10
+ import { Menu } from "lucide-react";
11
+
12
+ type TopNavProps = React.HTMLAttributes<HTMLElement> & {
13
+ links: {
14
+ title: string;
15
+ href: string;
16
+ isActive: boolean;
17
+ disabled?: boolean;
18
+ }[];
19
+ };
20
+
21
+ export function TopNav({ className, links, ...props }: TopNavProps) {
22
+ return (
23
+ <>
24
+ <DropdownMenu modal={false}>
25
+ <DropdownMenuTrigger
26
+ render={
27
+ <Button size="icon" variant="outline" className={cn("md:size-7 lg:hidden", className)}>
28
+ <Menu />
29
+ <span className="sr-only">Toggle navigation menu</span>
30
+ </Button>
31
+ }
32
+ />
33
+ <DropdownMenuContent side="bottom" align="start">
34
+ {links.map(({ title, href, isActive, disabled }) => (
35
+ <DropdownMenuItem
36
+ key={`${title}-${href}`}
37
+ render={
38
+ <Link
39
+ to={href}
40
+ className={!isActive ? "text-muted-foreground" : ""}
41
+ disabled={disabled}
42
+ >
43
+ {title}
44
+ </Link>
45
+ }
46
+ />
47
+ ))}
48
+ </DropdownMenuContent>
49
+ </DropdownMenu>
50
+
51
+ <nav
52
+ className={cn("hidden items-center space-x-4 lg:flex lg:space-x-4 xl:space-x-6", className)}
53
+ {...props}
54
+ >
55
+ {links.map(({ title, href, isActive, disabled }) => (
56
+ <Link
57
+ key={`${title}-${href}`}
58
+ to={href}
59
+ disabled={disabled}
60
+ className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? "" : "text-muted-foreground"}`}
61
+ >
62
+ {title}
63
+ </Link>
64
+ ))}
65
+ </nav>
66
+ </>
67
+ );
68
+ }
@@ -0,0 +1,44 @@
1
+ import { type LinkProps } from "@tanstack/react-router";
2
+
3
+ type User = {
4
+ name: string;
5
+ email: string;
6
+ avatar: string;
7
+ };
8
+
9
+ type Team = {
10
+ name: string;
11
+ logo: React.ElementType;
12
+ plan: string;
13
+ };
14
+
15
+ type BaseNavItem = {
16
+ title: string;
17
+ badge?: string;
18
+ icon?: React.ElementType;
19
+ };
20
+
21
+ type NavLink = BaseNavItem & {
22
+ url: LinkProps["to"] | (string & {});
23
+ items?: never;
24
+ };
25
+
26
+ type NavCollapsible = BaseNavItem & {
27
+ items: (BaseNavItem & { url: LinkProps["to"] | (string & {}) })[];
28
+ url?: never;
29
+ };
30
+
31
+ type NavItem = NavCollapsible | NavLink;
32
+
33
+ type NavGroup = {
34
+ title: string;
35
+ items: NavItem[];
36
+ };
37
+
38
+ type SidebarData = {
39
+ user: User;
40
+ teams: Team[];
41
+ navGroups: NavGroup[];
42
+ };
43
+
44
+ export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink };
@@ -0,0 +1,35 @@
1
+ import { Button } from "@icomponents/react/components/ui/button";
2
+ import { Popover, PopoverContent, PopoverTrigger } from "@icomponents/react/components/ui/popover";
3
+ import { cn } from "@icomponents/react/lib/utils";
4
+ import { CircleHelp } from "lucide-react";
5
+
6
+ type LearnMoreProps = Omit<React.ComponentProps<typeof Popover>, "children"> & {
7
+ children?: React.ReactNode;
8
+ contentProps?: React.ComponentProps<typeof PopoverContent>;
9
+ triggerProps?: React.ComponentProps<typeof PopoverTrigger>;
10
+ };
11
+
12
+ export function LearnMore({ children, contentProps, triggerProps, ...props }: LearnMoreProps) {
13
+ return (
14
+ <Popover {...props}>
15
+ <PopoverTrigger
16
+ {...triggerProps}
17
+ className={cn("size-5 rounded-full", triggerProps?.className)}
18
+ render={
19
+ <Button variant="outline" size="icon">
20
+ <span className="sr-only">Learn more</span>
21
+ <CircleHelp className="size-4 [&>circle]:hidden" />
22
+ </Button>
23
+ }
24
+ />
25
+ <PopoverContent
26
+ side="top"
27
+ align="start"
28
+ {...contentProps}
29
+ className={cn("text-sm text-muted-foreground", contentProps?.className)}
30
+ >
31
+ {children}
32
+ </PopoverContent>
33
+ </Popover>
34
+ );
35
+ }