@mostrom/app-shell 0.1.4 → 0.1.5
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/package.json +2 -2
- package/src/AppShell.tsx +0 -40
- package/src/components/global-header/GlobalHeaderSearch.tsx +0 -6
- package/src/components/global-header/ServicesMenu.tsx +49 -153
- package/src/components/global-header/AllServicesButton.tsx +0 -127
- package/src/components/global-header/CategoriesButton.tsx +0 -119
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostrom/app-shell",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@dnd-kit/utilities": "^3.2.2",
|
|
30
30
|
"@floating-ui/dom": "^1.7.5",
|
|
31
31
|
"@hookform/resolvers": "^5.2.2",
|
|
32
|
-
"@platform/service-catalog": "npm:@mostrom/service-catalog@^0.1.
|
|
32
|
+
"@platform/service-catalog": "npm:@mostrom/service-catalog@^0.1.5",
|
|
33
33
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
|
34
34
|
"@tanstack/react-table": "^8.21.3",
|
|
35
35
|
"class-variance-authority": "^0.7.1",
|
package/src/AppShell.tsx
CHANGED
|
@@ -157,12 +157,6 @@ export interface AppShellProps {
|
|
|
157
157
|
*/
|
|
158
158
|
appsMenuItems?: MenuDropdownItems;
|
|
159
159
|
|
|
160
|
-
/**
|
|
161
|
-
* Menu items for the Categories button.
|
|
162
|
-
* Displays the available service categories.
|
|
163
|
-
*/
|
|
164
|
-
categoriesMenuItems?: MenuDropdownItems;
|
|
165
|
-
|
|
166
160
|
/**
|
|
167
161
|
* Menu items for the Settings dropdown.
|
|
168
162
|
* Contains user preferences, notifications settings, and integrations.
|
|
@@ -178,9 +172,6 @@ export interface AppShellProps {
|
|
|
178
172
|
/** Callback when an item in the All Services menu is clicked */
|
|
179
173
|
onAppsMenuItemClick?: MenuItemClickHandler;
|
|
180
174
|
|
|
181
|
-
/** Callback when an item in the Categories menu is clicked */
|
|
182
|
-
onCategoriesMenuItemClick?: MenuItemClickHandler;
|
|
183
|
-
|
|
184
175
|
/** Callback when an item in the Settings menu is clicked */
|
|
185
176
|
onSettingsMenuItemClick?: MenuItemClickHandler;
|
|
186
177
|
|
|
@@ -315,11 +306,9 @@ export function AppShell({
|
|
|
315
306
|
searchValue,
|
|
316
307
|
searchPlaceholder,
|
|
317
308
|
appsMenuItems,
|
|
318
|
-
categoriesMenuItems,
|
|
319
309
|
settingsMenuItems,
|
|
320
310
|
userMenuItems,
|
|
321
311
|
onAppsMenuItemClick,
|
|
322
|
-
onCategoriesMenuItemClick,
|
|
323
312
|
onSettingsMenuItemClick,
|
|
324
313
|
onUserMenuItemClick,
|
|
325
314
|
onThemeToggle,
|
|
@@ -562,31 +551,6 @@ export function AppShell({
|
|
|
562
551
|
}));
|
|
563
552
|
}, [serviceCatalog, serviceGroups]);
|
|
564
553
|
|
|
565
|
-
const catalogCategoryMenuItems = React.useMemo<MenuDropdownItems>(() => {
|
|
566
|
-
if (!serviceCatalog) {
|
|
567
|
-
return [{ id: "loading", text: "Loading categories", disabled: true }];
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const categoryHrefById = new Map<string, string>();
|
|
571
|
-
|
|
572
|
-
for (const group of serviceGroups) {
|
|
573
|
-
const serviceHref = group.services[0]?.href;
|
|
574
|
-
if (serviceHref && serviceHref.startsWith("/")) {
|
|
575
|
-
const rootSegment = serviceHref.split("/").filter(Boolean)[0];
|
|
576
|
-
if (rootSegment) {
|
|
577
|
-
categoryHrefById.set(group.category.id, `/${rootSegment}/`);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return serviceCatalog.categories.map((category) => ({
|
|
583
|
-
id: category.id,
|
|
584
|
-
text: category.label,
|
|
585
|
-
description: category.description,
|
|
586
|
-
href: categoryHrefById.get(category.id),
|
|
587
|
-
}));
|
|
588
|
-
}, [serviceCatalog, serviceGroups]);
|
|
589
|
-
|
|
590
554
|
const hasSearchQuery = searchInputValue.trim().length > 0;
|
|
591
555
|
|
|
592
556
|
const searchResults = React.useMemo(() => {
|
|
@@ -667,8 +631,6 @@ export function AppShell({
|
|
|
667
631
|
) : undefined;
|
|
668
632
|
|
|
669
633
|
const resolvedAppsMenuItems = appsMenuItems ?? catalogMenuItems ?? EMPTY_MENU_ITEMS;
|
|
670
|
-
const resolvedCategoriesMenuItems =
|
|
671
|
-
categoriesMenuItems ?? catalogCategoryMenuItems ?? EMPTY_MENU_ITEMS;
|
|
672
634
|
const resolvedIdentity =
|
|
673
635
|
identity ?? {
|
|
674
636
|
logo: {
|
|
@@ -724,8 +686,6 @@ export function AppShell({
|
|
|
724
686
|
<GlobalHeaderSearch
|
|
725
687
|
appsMenuItems={resolvedAppsMenuItems}
|
|
726
688
|
onAppsMenuItemClick={onAppsMenuItemClick}
|
|
727
|
-
categoriesMenuItems={resolvedCategoriesMenuItems}
|
|
728
|
-
onCategoriesMenuItemClick={onCategoriesMenuItemClick}
|
|
729
689
|
searchValue={searchInputValue}
|
|
730
690
|
onSearchChange={handleSearchValueChange}
|
|
731
691
|
onSearchSelect={handleSearchSelect}
|
|
@@ -10,8 +10,6 @@ import {
|
|
|
10
10
|
export type GlobalHeaderSearchProps = {
|
|
11
11
|
appsMenuItems: MenuDropdownItems;
|
|
12
12
|
onAppsMenuItemClick?: MenuItemClickHandler;
|
|
13
|
-
categoriesMenuItems: MenuDropdownItems;
|
|
14
|
-
onCategoriesMenuItemClick?: MenuItemClickHandler;
|
|
15
13
|
searchValue: string;
|
|
16
14
|
onSearchChange: (value: string) => void;
|
|
17
15
|
onSearchSelect: (option: SearchOption) => void;
|
|
@@ -24,8 +22,6 @@ export type GlobalHeaderSearchProps = {
|
|
|
24
22
|
export function GlobalHeaderSearch({
|
|
25
23
|
appsMenuItems,
|
|
26
24
|
onAppsMenuItemClick,
|
|
27
|
-
categoriesMenuItems,
|
|
28
|
-
onCategoriesMenuItemClick,
|
|
29
25
|
searchValue,
|
|
30
26
|
onSearchChange,
|
|
31
27
|
onSearchSelect,
|
|
@@ -39,9 +35,7 @@ export function GlobalHeaderSearch({
|
|
|
39
35
|
<span className="app-shell-separator">|</span>
|
|
40
36
|
<ServicesMenu
|
|
41
37
|
appsMenuItems={appsMenuItems}
|
|
42
|
-
categoriesMenuItems={categoriesMenuItems}
|
|
43
38
|
onAppsMenuItemClick={onAppsMenuItemClick}
|
|
44
|
-
onCategoriesMenuItemClick={onCategoriesMenuItemClick}
|
|
45
39
|
/>
|
|
46
40
|
<GlobalSearch
|
|
47
41
|
value={searchValue}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, type MouseEvent } from "react";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
NavigationMenu,
|
|
5
|
+
NavigationMenuContent,
|
|
6
|
+
NavigationMenuItem,
|
|
7
|
+
NavigationMenuLink,
|
|
8
|
+
NavigationMenuList,
|
|
9
|
+
NavigationMenuTrigger,
|
|
10
|
+
} from "@/components/ui/navigation-menu";
|
|
4
11
|
|
|
5
12
|
// Menu dropdown types
|
|
6
13
|
export interface MenuDropdownItem {
|
|
@@ -31,9 +38,7 @@ export type MenuItemClickHandler = (detail: MenuItemClickDetail) => void;
|
|
|
31
38
|
|
|
32
39
|
export type ServicesMenuProps = {
|
|
33
40
|
appsMenuItems: MenuDropdownItems;
|
|
34
|
-
categoriesMenuItems: MenuDropdownItems;
|
|
35
41
|
onAppsMenuItemClick?: MenuItemClickHandler;
|
|
36
|
-
onCategoriesMenuItemClick?: MenuItemClickHandler;
|
|
37
42
|
};
|
|
38
43
|
|
|
39
44
|
type MenuItem = MenuDropdownItemOrGroup;
|
|
@@ -71,32 +76,11 @@ const flattenItems = (items: MenuDropdownItems): MenuDropdownItem[] => {
|
|
|
71
76
|
return result;
|
|
72
77
|
};
|
|
73
78
|
|
|
74
|
-
const normalizeGroups = (
|
|
75
|
-
items: MenuDropdownItems,
|
|
76
|
-
): Array<{ title?: string; items: MenuDropdownItem[] }> =>
|
|
77
|
-
items.map((item) => {
|
|
78
|
-
if (isGroup(item)) {
|
|
79
|
-
return { title: item.text, items: flattenItems(item.items) };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { title: undefined, items: [item as MenuDropdownItem] };
|
|
83
|
-
});
|
|
84
|
-
|
|
85
79
|
export function ServicesMenu({
|
|
86
80
|
appsMenuItems,
|
|
87
|
-
categoriesMenuItems,
|
|
88
81
|
onAppsMenuItemClick,
|
|
89
|
-
onCategoriesMenuItemClick,
|
|
90
82
|
}: ServicesMenuProps) {
|
|
91
|
-
const
|
|
92
|
-
const categories = useMemo(
|
|
93
|
-
() => flattenItems(categoriesMenuItems),
|
|
94
|
-
[categoriesMenuItems],
|
|
95
|
-
);
|
|
96
|
-
const groups = useMemo(
|
|
97
|
-
() => normalizeGroups(appsMenuItems),
|
|
98
|
-
[appsMenuItems],
|
|
99
|
-
);
|
|
83
|
+
const items = useMemo(() => flattenItems(appsMenuItems), [appsMenuItems]);
|
|
100
84
|
|
|
101
85
|
const handleItemClick =
|
|
102
86
|
(item: MenuDropdownItem) => (event: MouseEvent<HTMLElement>) => {
|
|
@@ -111,136 +95,48 @@ export function ServicesMenu({
|
|
|
111
95
|
href: item.href,
|
|
112
96
|
external: item.external,
|
|
113
97
|
});
|
|
114
|
-
|
|
115
|
-
setOpen(false);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const handleCategoryClick =
|
|
119
|
-
(item: MenuDropdownItem) => (event: MouseEvent<HTMLElement>) => {
|
|
120
|
-
if (item.disabled) {
|
|
121
|
-
event.preventDefault();
|
|
122
|
-
event.stopPropagation();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (onCategoriesMenuItemClick) {
|
|
127
|
-
event.preventDefault();
|
|
128
|
-
onCategoriesMenuItemClick({
|
|
129
|
-
id: item.id,
|
|
130
|
-
href: item.href,
|
|
131
|
-
external: item.external,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
setOpen(false);
|
|
136
98
|
};
|
|
137
99
|
|
|
138
100
|
return (
|
|
139
|
-
<
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<button
|
|
174
|
-
type="button"
|
|
175
|
-
className="app-shell-services-category"
|
|
176
|
-
disabled={category.disabled}
|
|
177
|
-
onClick={handleCategoryClick(category)}
|
|
178
|
-
>
|
|
179
|
-
{category.text}
|
|
180
|
-
</button>
|
|
181
|
-
)}
|
|
182
|
-
</li>
|
|
183
|
-
))}
|
|
184
|
-
</ul>
|
|
185
|
-
</div>
|
|
186
|
-
<div className="app-shell-services-divider" aria-hidden="true" />
|
|
187
|
-
<div className="app-shell-services-list" role="presentation">
|
|
188
|
-
{groups.map((group, groupIndex) => (
|
|
189
|
-
<div className="app-shell-services-group" key={`${group.title}-${groupIndex}`}>
|
|
190
|
-
{group.title ? (
|
|
191
|
-
<div className="app-shell-services-group-title">{group.title}</div>
|
|
192
|
-
) : null}
|
|
193
|
-
<ul>
|
|
194
|
-
{group.items.map((item) => {
|
|
195
|
-
const isDisabled = Boolean(item.disabled);
|
|
196
|
-
const content = (
|
|
197
|
-
<>
|
|
198
|
-
<span className="app-shell-services-item-text">{item.text}</span>
|
|
199
|
-
{item.description ? (
|
|
200
|
-
<span className="app-shell-services-item-description">
|
|
201
|
-
{item.description}
|
|
202
|
-
</span>
|
|
203
|
-
) : null}
|
|
204
|
-
</>
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
if (item.href) {
|
|
208
|
-
return (
|
|
209
|
-
<li key={item.id}>
|
|
210
|
-
<a
|
|
211
|
-
className="app-shell-services-item"
|
|
212
|
-
href={item.href}
|
|
213
|
-
target={item.external ? "_blank" : undefined}
|
|
214
|
-
rel={item.external ? "noopener noreferrer" : undefined}
|
|
215
|
-
aria-disabled={isDisabled || undefined}
|
|
216
|
-
onClick={handleItemClick(item)}
|
|
217
|
-
>
|
|
218
|
-
{content}
|
|
219
|
-
</a>
|
|
220
|
-
</li>
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return (
|
|
225
|
-
<li key={item.id}>
|
|
226
|
-
<button
|
|
227
|
-
type="button"
|
|
228
|
-
className="app-shell-services-item"
|
|
229
|
-
disabled={isDisabled}
|
|
230
|
-
onClick={handleItemClick(item)}
|
|
231
|
-
>
|
|
232
|
-
{content}
|
|
233
|
-
</button>
|
|
234
|
-
</li>
|
|
235
|
-
);
|
|
236
|
-
})}
|
|
237
|
-
</ul>
|
|
238
|
-
</div>
|
|
101
|
+
<NavigationMenu>
|
|
102
|
+
<NavigationMenuList>
|
|
103
|
+
<NavigationMenuItem>
|
|
104
|
+
<NavigationMenuTrigger className="app-shell-apps-button h-9 w-9 p-0 bg-transparent hover:bg-transparent data-[state=open]:bg-transparent">
|
|
105
|
+
<span className="app-shell-apps-icon">
|
|
106
|
+
<DotsNineIcon />
|
|
107
|
+
</span>
|
|
108
|
+
</NavigationMenuTrigger>
|
|
109
|
+
<NavigationMenuContent>
|
|
110
|
+
<ul className="grid w-[400px] gap-2 p-2 md:w-[500px] md:grid-cols-2">
|
|
111
|
+
{items.map((item) => (
|
|
112
|
+
<li key={item.id}>
|
|
113
|
+
<NavigationMenuLink
|
|
114
|
+
asChild
|
|
115
|
+
className="flex flex-col items-start gap-1 p-3 rounded-md hover:bg-accent"
|
|
116
|
+
>
|
|
117
|
+
<a
|
|
118
|
+
href={item.href || "#"}
|
|
119
|
+
target={item.external ? "_blank" : undefined}
|
|
120
|
+
rel={item.external ? "noopener noreferrer" : undefined}
|
|
121
|
+
aria-disabled={item.disabled || undefined}
|
|
122
|
+
onClick={handleItemClick(item)}
|
|
123
|
+
>
|
|
124
|
+
<div className="text-sm leading-none font-medium">
|
|
125
|
+
{item.text}
|
|
126
|
+
</div>
|
|
127
|
+
{item.description && (
|
|
128
|
+
<p className="text-muted-foreground line-clamp-2 text-sm leading-snug">
|
|
129
|
+
{item.description}
|
|
130
|
+
</p>
|
|
131
|
+
)}
|
|
132
|
+
</a>
|
|
133
|
+
</NavigationMenuLink>
|
|
134
|
+
</li>
|
|
239
135
|
))}
|
|
240
|
-
</
|
|
241
|
-
</
|
|
242
|
-
</
|
|
243
|
-
</
|
|
244
|
-
</
|
|
136
|
+
</ul>
|
|
137
|
+
</NavigationMenuContent>
|
|
138
|
+
</NavigationMenuItem>
|
|
139
|
+
</NavigationMenuList>
|
|
140
|
+
</NavigationMenu>
|
|
245
141
|
);
|
|
246
142
|
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React from "react";
|
|
4
|
-
import { Button } from "../ui/button";
|
|
5
|
-
import {
|
|
6
|
-
DropdownMenu,
|
|
7
|
-
DropdownMenuContent,
|
|
8
|
-
DropdownMenuGroup,
|
|
9
|
-
DropdownMenuItem,
|
|
10
|
-
DropdownMenuLabel,
|
|
11
|
-
DropdownMenuSeparator,
|
|
12
|
-
DropdownMenuTrigger,
|
|
13
|
-
} from "../ui/dropdown-menu";
|
|
14
|
-
import type {
|
|
15
|
-
MenuDropdownItems,
|
|
16
|
-
MenuDropdownItem,
|
|
17
|
-
MenuDropdownItemGroup,
|
|
18
|
-
MenuItemClickHandler,
|
|
19
|
-
} from "./ServicesMenu";
|
|
20
|
-
|
|
21
|
-
type AllServicesButtonProps = {
|
|
22
|
-
items: MenuDropdownItems;
|
|
23
|
-
onItemClick?: MenuItemClickHandler;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const DotsNineIcon = () => (
|
|
27
|
-
<svg
|
|
28
|
-
aria-hidden="true"
|
|
29
|
-
focusable="false"
|
|
30
|
-
width="24"
|
|
31
|
-
height="24"
|
|
32
|
-
viewBox="0 0 24 24"
|
|
33
|
-
>
|
|
34
|
-
{([5, 12, 19] as const).flatMap((cx) =>
|
|
35
|
-
([5, 12, 19] as const).map((cy) => (
|
|
36
|
-
<circle key={`${cx}-${cy}`} cx={cx} cy={cy} r={1.6} fill="currentColor" />
|
|
37
|
-
)),
|
|
38
|
-
)}
|
|
39
|
-
</svg>
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const isGroup = (item: MenuDropdownItem | MenuDropdownItemGroup): item is MenuDropdownItemGroup =>
|
|
43
|
-
typeof (item as MenuDropdownItemGroup).items !== "undefined";
|
|
44
|
-
|
|
45
|
-
export function AllServicesButton({ items, onItemClick }: AllServicesButtonProps) {
|
|
46
|
-
const handleItemClick = (item: MenuDropdownItem) => {
|
|
47
|
-
if (item.disabled) return;
|
|
48
|
-
onItemClick?.({
|
|
49
|
-
id: item.id,
|
|
50
|
-
href: item.href,
|
|
51
|
-
external: item.external,
|
|
52
|
-
});
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const renderItem = (item: MenuDropdownItem) => {
|
|
56
|
-
const content = (
|
|
57
|
-
<>
|
|
58
|
-
<span>{item.text}</span>
|
|
59
|
-
{item.description && (
|
|
60
|
-
<span className="text-xs text-muted-foreground ml-2">{item.description}</span>
|
|
61
|
-
)}
|
|
62
|
-
</>
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
if (item.href) {
|
|
66
|
-
return (
|
|
67
|
-
<DropdownMenuItem key={item.id} asChild disabled={item.disabled}>
|
|
68
|
-
<a
|
|
69
|
-
href={item.href}
|
|
70
|
-
target={item.external ? "_blank" : undefined}
|
|
71
|
-
rel={item.external ? "noopener noreferrer" : undefined}
|
|
72
|
-
onClick={() => handleItemClick(item)}
|
|
73
|
-
>
|
|
74
|
-
{content}
|
|
75
|
-
</a>
|
|
76
|
-
</DropdownMenuItem>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<DropdownMenuItem
|
|
82
|
-
key={item.id}
|
|
83
|
-
disabled={item.disabled}
|
|
84
|
-
onSelect={() => handleItemClick(item)}
|
|
85
|
-
>
|
|
86
|
-
{content}
|
|
87
|
-
</DropdownMenuItem>
|
|
88
|
-
);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<div className="app-shell-apps-button">
|
|
93
|
-
<DropdownMenu>
|
|
94
|
-
<DropdownMenuTrigger asChild>
|
|
95
|
-
<Button
|
|
96
|
-
variant="ghost"
|
|
97
|
-
size="icon"
|
|
98
|
-
aria-label="Apps"
|
|
99
|
-
className="h-9 w-9 hover:bg-transparent"
|
|
100
|
-
>
|
|
101
|
-
<span className="app-shell-apps-icon">
|
|
102
|
-
<DotsNineIcon />
|
|
103
|
-
</span>
|
|
104
|
-
</Button>
|
|
105
|
-
</DropdownMenuTrigger>
|
|
106
|
-
<DropdownMenuContent align="start" sideOffset={8} className="w-64 max-h-96 overflow-y-auto">
|
|
107
|
-
{items.map((item, index) => {
|
|
108
|
-
if (isGroup(item)) {
|
|
109
|
-
return (
|
|
110
|
-
<React.Fragment key={item.text || `group-${index}`}>
|
|
111
|
-
{index > 0 && <DropdownMenuSeparator />}
|
|
112
|
-
<DropdownMenuGroup>
|
|
113
|
-
{item.text && (
|
|
114
|
-
<DropdownMenuLabel>{item.text}</DropdownMenuLabel>
|
|
115
|
-
)}
|
|
116
|
-
{item.items.map((subItem) => renderItem(subItem as MenuDropdownItem))}
|
|
117
|
-
</DropdownMenuGroup>
|
|
118
|
-
</React.Fragment>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
return renderItem(item as MenuDropdownItem);
|
|
122
|
-
})}
|
|
123
|
-
</DropdownMenuContent>
|
|
124
|
-
</DropdownMenu>
|
|
125
|
-
</div>
|
|
126
|
-
);
|
|
127
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Button } from "../ui/button";
|
|
4
|
-
import {
|
|
5
|
-
DropdownMenu,
|
|
6
|
-
DropdownMenuContent,
|
|
7
|
-
DropdownMenuItem,
|
|
8
|
-
DropdownMenuTrigger,
|
|
9
|
-
} from "../ui/dropdown-menu";
|
|
10
|
-
import type {
|
|
11
|
-
MenuDropdownItems,
|
|
12
|
-
MenuDropdownItem,
|
|
13
|
-
MenuDropdownItemGroup,
|
|
14
|
-
MenuItemClickHandler,
|
|
15
|
-
} from "./ServicesMenu";
|
|
16
|
-
|
|
17
|
-
type CategoriesButtonProps = {
|
|
18
|
-
items: MenuDropdownItems;
|
|
19
|
-
onItemClick?: MenuItemClickHandler;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const SquaresFourIcon = () => (
|
|
23
|
-
<svg
|
|
24
|
-
aria-hidden="true"
|
|
25
|
-
focusable="false"
|
|
26
|
-
width="22"
|
|
27
|
-
height="22"
|
|
28
|
-
viewBox="0 0 24 24"
|
|
29
|
-
>
|
|
30
|
-
<rect x="4" y="4" width="7" height="7" rx="1.4" fill="currentColor" />
|
|
31
|
-
<rect x="13" y="4" width="7" height="7" rx="1.4" fill="currentColor" />
|
|
32
|
-
<rect x="4" y="13" width="7" height="7" rx="1.4" fill="currentColor" />
|
|
33
|
-
<rect x="13" y="13" width="7" height="7" rx="1.4" fill="currentColor" />
|
|
34
|
-
</svg>
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const isGroup = (item: MenuDropdownItem | MenuDropdownItemGroup): item is MenuDropdownItemGroup =>
|
|
38
|
-
typeof (item as MenuDropdownItemGroup).items !== "undefined";
|
|
39
|
-
|
|
40
|
-
const flattenItems = (items: MenuDropdownItems): MenuDropdownItem[] => {
|
|
41
|
-
const result: MenuDropdownItem[] = [];
|
|
42
|
-
items.forEach((item) => {
|
|
43
|
-
if (isGroup(item)) {
|
|
44
|
-
result.push(...flattenItems(item.items));
|
|
45
|
-
} else {
|
|
46
|
-
result.push(item as MenuDropdownItem);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
return result;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export function CategoriesButton({ items, onItemClick }: CategoriesButtonProps) {
|
|
53
|
-
const flatItems = flattenItems(items);
|
|
54
|
-
|
|
55
|
-
const handleItemClick = (item: MenuDropdownItem) => {
|
|
56
|
-
if (item.disabled) return;
|
|
57
|
-
onItemClick?.({
|
|
58
|
-
id: item.id,
|
|
59
|
-
href: item.href,
|
|
60
|
-
external: item.external,
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<div className="app-shell-categories-button">
|
|
66
|
-
<DropdownMenu>
|
|
67
|
-
<DropdownMenuTrigger asChild>
|
|
68
|
-
<Button
|
|
69
|
-
variant="ghost"
|
|
70
|
-
size="icon"
|
|
71
|
-
aria-label="Categories"
|
|
72
|
-
className="h-9 w-9 hover:bg-transparent"
|
|
73
|
-
>
|
|
74
|
-
<span className="app-shell-categories-icon">
|
|
75
|
-
<SquaresFourIcon />
|
|
76
|
-
</span>
|
|
77
|
-
</Button>
|
|
78
|
-
</DropdownMenuTrigger>
|
|
79
|
-
<DropdownMenuContent align="start" sideOffset={8} className="w-56">
|
|
80
|
-
{flatItems.map((item) => {
|
|
81
|
-
const content = (
|
|
82
|
-
<>
|
|
83
|
-
<span>{item.text}</span>
|
|
84
|
-
{item.description && (
|
|
85
|
-
<span className="text-xs text-muted-foreground ml-2">{item.description}</span>
|
|
86
|
-
)}
|
|
87
|
-
</>
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
if (item.href) {
|
|
91
|
-
return (
|
|
92
|
-
<DropdownMenuItem key={item.id} asChild disabled={item.disabled}>
|
|
93
|
-
<a
|
|
94
|
-
href={item.href}
|
|
95
|
-
target={item.external ? "_blank" : undefined}
|
|
96
|
-
rel={item.external ? "noopener noreferrer" : undefined}
|
|
97
|
-
onClick={() => handleItemClick(item)}
|
|
98
|
-
>
|
|
99
|
-
{content}
|
|
100
|
-
</a>
|
|
101
|
-
</DropdownMenuItem>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<DropdownMenuItem
|
|
107
|
-
key={item.id}
|
|
108
|
-
disabled={item.disabled}
|
|
109
|
-
onSelect={() => handleItemClick(item)}
|
|
110
|
-
>
|
|
111
|
-
{content}
|
|
112
|
-
</DropdownMenuItem>
|
|
113
|
-
);
|
|
114
|
-
})}
|
|
115
|
-
</DropdownMenuContent>
|
|
116
|
-
</DropdownMenu>
|
|
117
|
-
</div>
|
|
118
|
-
);
|
|
119
|
-
}
|