@c-rex/components 0.1.38 → 0.1.39

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 (71) hide show
  1. package/README.md +73 -73
  2. package/package.json +250 -218
  3. package/src/article/article-action-bar.tsx +110 -110
  4. package/src/article/article-content.tsx +18 -46
  5. package/src/autocomplete.tsx +201 -201
  6. package/src/breadcrumb.tsx +124 -124
  7. package/src/carousel/carousel.tsx +353 -353
  8. package/src/check-article-lang.tsx +47 -47
  9. package/src/directoryNodes/directory-tree-context.tsx +388 -0
  10. package/src/directoryNodes/tree-of-content.tsx +68 -67
  11. package/src/documents/result-list.tsx +124 -127
  12. package/src/favorites/bookmark-button.tsx +97 -94
  13. package/src/favorites/favorite-button.tsx +137 -120
  14. package/src/footer/footer-shell.tsx +52 -0
  15. package/src/footer/footer.tsx +7 -0
  16. package/src/footer/legal-links-block.tsx +25 -0
  17. package/src/footer/organization-contact-block.tsx +94 -0
  18. package/src/footer/social-links-block.tsx +38 -0
  19. package/src/footer/types.ts +10 -0
  20. package/src/footer/vcard-footer.tsx +72 -0
  21. package/src/generated/client-components.tsx +1366 -1350
  22. package/src/generated/create-client-request.tsx +116 -113
  23. package/src/generated/create-server-request.tsx +70 -61
  24. package/src/generated/create-suggestions-request.tsx +55 -55
  25. package/src/generated/server-components.tsx +1056 -1056
  26. package/src/generated/suggestions.tsx +302 -299
  27. package/src/icons/file-icon.tsx +8 -8
  28. package/src/icons/flag-icon.tsx +15 -15
  29. package/src/icons/loading.tsx +11 -11
  30. package/src/icons/social-icon.tsx +24 -0
  31. package/src/info/info-card.tsx +43 -0
  32. package/src/info/{info-table.tsx → information-unit-metadata-grid.tsx} +157 -168
  33. package/src/info/shared.tsx +49 -25
  34. package/src/navbar/language-switcher/content-language-switch.tsx +92 -92
  35. package/src/navbar/language-switcher/shared.tsx +33 -33
  36. package/src/navbar/language-switcher/ui-language-switch.tsx +37 -37
  37. package/src/navbar/navbar.tsx +157 -152
  38. package/src/navbar/settings.tsx +62 -62
  39. package/src/navbar/sign-in-out-btns.tsx +35 -35
  40. package/src/navbar/user-menu.tsx +60 -60
  41. package/src/page-wrapper.tsx +54 -31
  42. package/src/render-article.module.css +155 -0
  43. package/src/render-article.tsx +75 -68
  44. package/src/renditions/file-download.tsx +83 -83
  45. package/src/renditions/html.tsx +64 -64
  46. package/src/renditions/image/container.tsx +54 -54
  47. package/src/renditions/image/rendition.tsx +55 -55
  48. package/src/restriction-menu/restriction-menu-container.tsx +117 -53
  49. package/src/restriction-menu/restriction-menu-item.tsx +155 -147
  50. package/src/restriction-menu/restriction-menu.tsx +341 -156
  51. package/src/results/dialog-filter.tsx +166 -166
  52. package/src/results/empty.tsx +15 -15
  53. package/src/results/filter-navbar.tsx +294 -261
  54. package/src/results/filter-sidebar/__tests__/utils.test.ts +129 -0
  55. package/src/results/filter-sidebar/index.tsx +270 -126
  56. package/src/results/filter-sidebar/utils.ts +196 -164
  57. package/src/results/generic/table-result-list.tsx +97 -99
  58. package/src/results/{table-with-images.tsx → information-unit-search-results-card-list.tsx} +125 -127
  59. package/src/results/{cards.tsx → information-unit-search-results-cards.tsx} +99 -99
  60. package/src/results/{table.tsx → information-unit-search-results-table.tsx} +104 -104
  61. package/src/results/pagination.tsx +81 -81
  62. package/src/results/summary.ts +30 -0
  63. package/src/results/utils.ts +54 -54
  64. package/src/search-input.tsx +70 -70
  65. package/src/share-button.tsx +49 -49
  66. package/src/stores/favorites-store.ts +88 -88
  67. package/src/stores/highlight-store.ts +15 -15
  68. package/src/stores/language-store.ts +14 -14
  69. package/src/stores/restriction-store.ts +11 -11
  70. package/src/stores/search-settings-store.ts +68 -64
  71. package/src/info/set-available-versions.tsx +0 -19
@@ -0,0 +1,129 @@
1
+ import { Tags } from "@c-rex/interfaces";
2
+ import {
3
+ clearData,
4
+ memoizeFilteredTags,
5
+ resolveLabelByLanguage,
6
+ updateFilterParam,
7
+ removeFilterItem,
8
+ } from "../utils";
9
+
10
+ const createTags = (): Tags => ({
11
+ informationSubjects: {
12
+ labels: ["Information Subjects"],
13
+ items: [
14
+ {
15
+ hits: 3,
16
+ total: 10,
17
+ labels: [
18
+ { language: "en", value: "Translation" },
19
+ { language: "de", value: "Uebersetzung" },
20
+ ],
21
+ shortId: "subA",
22
+ id: "https://example.org/subA",
23
+ } as unknown as Tags[string]["items"][number],
24
+ {
25
+ hits: 0,
26
+ total: 5,
27
+ labels: [{ language: "en", value: "Collection" }],
28
+ shortId: "subB",
29
+ id: "https://example.org/subB",
30
+ } as unknown as Tags[string]["items"][number],
31
+ ],
32
+ },
33
+ packages: {
34
+ labels: ["Packages"],
35
+ items: [
36
+ {
37
+ hits: 2,
38
+ total: 2,
39
+ labels: [{ language: "en", value: "Default Package" }],
40
+ shortId: "pkgA",
41
+ id: "https://example.org/pkgA",
42
+ } as unknown as Tags[string]["items"][number],
43
+ ],
44
+ },
45
+ });
46
+
47
+ describe("filter-sidebar utils", () => {
48
+ it("resolveLabelByLanguage prefers exact language, then base, then fallback", () => {
49
+ const labels = [
50
+ { language: "en", value: "English Label" },
51
+ { language: "de", value: "German Label" },
52
+ { value: "Neutral Label" },
53
+ ];
54
+
55
+ expect(resolveLabelByLanguage(labels, "de-DE")).toBe("German Label");
56
+ expect(resolveLabelByLanguage(labels, "en-US")).toBe("English Label");
57
+ expect(resolveLabelByLanguage([{ value: "Neutral Label" }], "fr-FR")).toBe("Neutral Label");
58
+ expect(resolveLabelByLanguage([], "en")).toBeUndefined();
59
+ });
60
+
61
+ it("clearData includes zero-hit entries by default", () => {
62
+ const tags = createTags();
63
+ const result = clearData(tags, { uiLanguage: "en" });
64
+
65
+ expect(result.informationSubjects).toHaveLength(2);
66
+ expect(result.informationSubjects?.map((item) => item.shortId)).toEqual(["subA", "subB"]);
67
+ });
68
+
69
+ it("clearData excludes zero-hit entries when includeZeroHits is false", () => {
70
+ const tags = createTags();
71
+ const result = clearData(tags, { uiLanguage: "en", includeZeroHits: false });
72
+
73
+ expect(result.informationSubjects).toHaveLength(1);
74
+ expect(result.informationSubjects?.[0]?.shortId).toBe("subA");
75
+ });
76
+
77
+ it("clearData applies facet overrides and stores taxonomyId", () => {
78
+ const tags = createTags();
79
+ const result = clearData(tags, {
80
+ uiLanguage: "en",
81
+ labelOverrides: {
82
+ informationSubjects: {
83
+ subA: {
84
+ label: "Overridden Label",
85
+ groupLabel: "Subject Group",
86
+ sectionLabel: "Process",
87
+ },
88
+ },
89
+ },
90
+ });
91
+
92
+ expect(result.informationSubjects?.[0]).toMatchObject({
93
+ label: "Overridden Label",
94
+ groupLabel: "Subject Group",
95
+ sectionLabel: "Process",
96
+ taxonomyId: "https://example.org/subA",
97
+ });
98
+ });
99
+
100
+ it("memoizeFilteredTags marks active filters and active package", () => {
101
+ const tags = createTags();
102
+ const result = memoizeFilteredTags(tags, "informationSubjects.shortId=subA", "pkgA", {
103
+ uiLanguage: "en",
104
+ });
105
+
106
+ expect(result.informationSubjects?.find((item) => item.shortId === "subA")?.active).toBe(true);
107
+ expect(result.informationSubjects?.find((item) => item.shortId === "subB")?.active).toBe(false);
108
+ expect(result.packages?.find((item) => item.shortId === "pkgA")?.active).toBe(true);
109
+ });
110
+
111
+ it("updateFilterParam and removeFilterItem keep query behavior stable", () => {
112
+ expect(updateFilterParam("packages", { shortId: "pkgA", hits: 0, total: 0, label: "", active: false }, null)).toEqual({
113
+ packages: "pkgA",
114
+ });
115
+
116
+ expect(updateFilterParam("informationSubjects", { shortId: "subA", hits: 0, total: 0, label: "", active: false }, null)).toEqual({
117
+ filter: "informationSubjects.shortId=subA",
118
+ });
119
+
120
+ expect(
121
+ removeFilterItem(
122
+ "informationSubjects",
123
+ { shortId: "subA", hits: 0, total: 0, label: "", active: false },
124
+ "informationSubjects.shortId=subA,packages.shortId=pkgA"
125
+ )
126
+ ).toEqual({ filter: "packages.shortId=pkgA" });
127
+ });
128
+ });
129
+
@@ -1,126 +1,270 @@
1
- "use client";
2
-
3
- import { FC, useMemo } from "react";
4
- import { useTranslations } from 'next-intl'
5
- import { Check, ChevronDown } from "lucide-react"
6
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@c-rex/ui/collapsible";
7
- import {
8
- SidebarContent,
9
- SidebarGroup,
10
- SidebarGroupContent,
11
- SidebarGroupLabel,
12
- SidebarHeader,
13
- SidebarMenu,
14
- SidebarMenuSub,
15
- SidebarMenuSubButton,
16
- SidebarMenuSubItem
17
- } from "@c-rex/ui/sidebar";
18
- import { parseAsString, useQueryStates } from "nuqs";
19
- import { memoizeFilteredTags, removeFilterItem, updateFilterParam } from "./utils";
20
- import { FilterItem, Tags } from "@c-rex/interfaces";
21
-
22
-
23
- export interface FilterSidebarProps {
24
- tags?: Tags
25
- totalItemCount?: number
26
- }
27
-
28
-
29
- //TODO; check layout on mobile
30
- export const FilterSidebar: FC<FilterSidebarProps> = ({ tags, totalItemCount }) => {
31
- const t = useTranslations();
32
- const [params, setParams] = useQueryStates({
33
- packages: parseAsString,
34
- filter: parseAsString,
35
- }, {
36
- history: 'push',
37
- shallow: false,
38
- });
39
-
40
- const filteredTags = useMemo(() => {
41
- return memoizeFilteredTags(tags, params.filter, params.packages);
42
- }, [tags, params.filter, params.packages]);
43
-
44
- const onClickHandler = (key: string, item: FilterItem) => {
45
- if (item.active) {
46
- setParams(removeFilterItem(key, item, params.filter))
47
- return;
48
- }
49
-
50
- setParams(updateFilterParam(key, item, params.filter))
51
- }
52
-
53
- const content = (
54
- <SidebarContent className="!gap-0" suppressHydrationWarning>
55
-
56
- {Object.entries(filteredTags).map(([key, value]) => (
57
-
58
- <Collapsible defaultOpen key={key} className="py-0 group/collapsible">
59
- <SidebarGroup>
60
-
61
- <SidebarGroupLabel asChild className="hover:bg-sidebar-accent text-sidebar-accent-foreground text-sm font-bold">
62
- <CollapsibleTrigger className="!h-9">
63
- {t(`filter.tags.${key}`)}
64
- <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
65
- </CollapsibleTrigger>
66
- </SidebarGroupLabel>
67
-
68
- <CollapsibleContent>
69
- <SidebarGroupContent>
70
- <SidebarMenu>
71
- <SidebarMenuSub>
72
- {value.map((item) => (
73
- <SidebarMenuSubItem key={item.shortId}>
74
- <SidebarMenuSubButton
75
- className="cursor-pointer"
76
- isActive={item.active}
77
- onClick={() => onClickHandler(key, item)}
78
- >
79
- {item.label} ({item.hits}/{item.total})
80
- {item.active && <Check className="ml-2" />}
81
- </SidebarMenuSubButton>
82
- </SidebarMenuSubItem>
83
- ))}
84
- </SidebarMenuSub>
85
- </SidebarMenu>
86
- </SidebarGroupContent>
87
- </CollapsibleContent>
88
- </SidebarGroup>
89
- </Collapsible>
90
- ))}
91
- </SidebarContent>
92
- )
93
-
94
- /*
95
- if (isMobile) {
96
- return (
97
-
98
- <SheetContent
99
- side="left"
100
- className="!pt-6 !px-2 w-[400px] overflow-y-auto"
101
- >
102
- <SheetHeader className="justify-center items-end font-bold">
103
- {t("filter.filters")}
104
- <span className="text-xs text-muted-foreground leading-5">
105
- {totalItemCount} {t("results.results")}
106
- </span>
107
- </SheetHeader>
108
- {content}
109
- </SheetContent>
110
- )
111
- }
112
- */
113
- if (Object.entries(filteredTags).length === 0) return null;
114
-
115
- return (
116
- <div className="w-60 lg:w-80 bg-sidebar rounded-md border pb-4">
117
- <SidebarHeader className="justify-center items-end font-bold">
118
- {t("filter.filters")}
119
- <span className="text-xs text-muted-foreground leading-5">
120
- {totalItemCount} {t("results.results")}
121
- </span>
122
- </SidebarHeader>
123
- {content}
124
- </div>
125
- );
126
- };
1
+ "use client";
2
+
3
+ import { FC, useMemo } from "react";
4
+ import { useLocale, useTranslations } from 'next-intl'
5
+ import { Check, ChevronDown } from "lucide-react"
6
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@c-rex/ui/collapsible";
7
+ import {
8
+ SidebarContent,
9
+ SidebarGroup,
10
+ SidebarGroupContent,
11
+ SidebarGroupLabel,
12
+ SidebarHeader,
13
+ SidebarMenu,
14
+ SidebarMenuSub,
15
+ SidebarMenuSubButton,
16
+ SidebarMenuSubItem
17
+ } from "@c-rex/ui/sidebar";
18
+ import { parseAsString, useQueryStates } from "nuqs";
19
+ import { applyFacetPropertyVisibility, FacetLabelOverrides, memoizeFilteredTags, removeFilterItem, ResolvedFilterItem, resolveLabelByLanguage, updateFilterParam } from "./utils";
20
+ import { FilterItem, Tags } from "@c-rex/interfaces";
21
+
22
+
23
+ export interface FilterSidebarProps {
24
+ tags?: Tags
25
+ totalItemCount?: number
26
+ facetLabelOverrides?: FacetLabelOverrides
27
+ includeZeroHits?: boolean
28
+ includeProperties?: string[]
29
+ excludeProperties?: string[]
30
+ }
31
+
32
+
33
+ //TODO; check layout on mobile
34
+ export const FilterSidebar: FC<FilterSidebarProps> = ({
35
+ tags,
36
+ totalItemCount,
37
+ facetLabelOverrides,
38
+ includeZeroHits = true,
39
+ includeProperties,
40
+ excludeProperties,
41
+ }) => {
42
+ const t = useTranslations();
43
+ const locale = useLocale();
44
+ const [params, setParams] = useQueryStates({
45
+ packages: parseAsString,
46
+ filter: parseAsString,
47
+ }, {
48
+ history: 'push',
49
+ shallow: false,
50
+ });
51
+
52
+ const filteredTags = useMemo(() => {
53
+ const resolved = memoizeFilteredTags(tags, params.filter, params.packages, {
54
+ uiLanguage: locale,
55
+ labelOverrides: facetLabelOverrides,
56
+ includeZeroHits,
57
+ });
58
+ return applyFacetPropertyVisibility(resolved, includeProperties, excludeProperties);
59
+ }, [tags, params.filter, params.packages, locale, facetLabelOverrides, includeZeroHits, includeProperties, excludeProperties]);
60
+
61
+ const onClickHandler = (key: string, item: FilterItem) => {
62
+ if (item.active) {
63
+ setParams(removeFilterItem(key, item, params.filter))
64
+ return;
65
+ }
66
+
67
+ setParams(updateFilterParam(key, item, params.filter))
68
+ }
69
+
70
+ const resolvePropertyLabel = (propertyKey: string): string => {
71
+ const rawLabels = (tags?.[propertyKey]?.labels as unknown[] | undefined) || [];
72
+ const normalized = rawLabels
73
+ .map((item) => {
74
+ if (typeof item === "string") {
75
+ return { value: item };
76
+ }
77
+
78
+ if (item && typeof item === "object") {
79
+ const labelObj = item as { language?: string; value?: string };
80
+ if (typeof labelObj.value === "string") {
81
+ return { language: labelObj.language, value: labelObj.value };
82
+ }
83
+ }
84
+
85
+ return undefined;
86
+ })
87
+ .filter((item): item is { language?: string; value: string } => Boolean(item));
88
+
89
+ return resolveLabelByLanguage(normalized, locale) || propertyKey;
90
+ };
91
+
92
+ const content = (
93
+ <SidebarContent className="!gap-0" suppressHydrationWarning>
94
+
95
+ {Object.entries(filteredTags).map(([key, value]) => {
96
+ const items = value as ResolvedFilterItem[];
97
+ const hasSectionLabels = items.some((item) => Boolean(item.sectionLabel));
98
+ const propertyLabel = resolvePropertyLabel(key);
99
+
100
+ return (
101
+ <div
102
+ key={key}
103
+ data-filter-group-key={key}
104
+ className="border-b border-border/40 pb-1 mb-1 last:border-b-0 last:mb-0"
105
+ >
106
+ {!hasSectionLabels && (
107
+ <Collapsible defaultOpen className="py-0 group/collapsible">
108
+ <SidebarGroup className="!p-1">
109
+ <SidebarGroupLabel asChild className="hover:bg-sidebar-accent text-sidebar-accent-foreground text-sm font-bold">
110
+ <CollapsibleTrigger className="!h-8">
111
+ {propertyLabel}
112
+ <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
113
+ </CollapsibleTrigger>
114
+ </SidebarGroupLabel>
115
+ <CollapsibleContent>
116
+ <SidebarGroupContent>
117
+ <SidebarMenu className="!gap-0.5">
118
+ <SidebarMenuSub className="!ml-2 !pl-1.5 !py-0 !gap-0.5">
119
+ {items.map((item) => (
120
+ <SidebarMenuSubItem key={item.shortId}>
121
+ <SidebarMenuSubButton
122
+ className="cursor-pointer !py-1.5"
123
+ data-taxonomy-id={item.taxonomyId}
124
+ data-taxonomy-short-id={item.shortId}
125
+ isActive={item.active}
126
+ onClick={() => onClickHandler(key, item)}
127
+ >
128
+ {item.label} ({item.hits}/{item.total})
129
+ {item.active && <Check className="ml-2" />}
130
+ </SidebarMenuSubButton>
131
+ </SidebarMenuSubItem>
132
+ ))}
133
+ </SidebarMenuSub>
134
+ </SidebarMenu>
135
+ </SidebarGroupContent>
136
+ </CollapsibleContent>
137
+ </SidebarGroup>
138
+ </Collapsible>
139
+ )}
140
+
141
+ {hasSectionLabels && (
142
+ <>
143
+ <div className="sr-only">{propertyLabel}</div>
144
+ {Object.entries(
145
+ items.reduce<Record<string, ResolvedFilterItem[]>>((acc, item) => {
146
+ const sectionLabel = item.sectionLabel || "Other";
147
+ if (!acc[sectionLabel]) acc[sectionLabel] = [];
148
+ acc[sectionLabel].push(item);
149
+ return acc;
150
+ }, {})
151
+ )
152
+ .sort(([a], [b]) => a.localeCompare(b))
153
+ .map(([sectionLabel, sectionItems]) => {
154
+ const hasNestedGroups = sectionItems.some((item) => Boolean(item.groupLabel));
155
+ return (
156
+ <Collapsible
157
+ defaultOpen
158
+ key={`${key}:${sectionLabel}`}
159
+ className="py-0 group/collapsible"
160
+ >
161
+ <SidebarGroup className="!p-1">
162
+ <SidebarGroupLabel
163
+ asChild
164
+ className="hover:bg-sidebar-accent text-sidebar-accent-foreground text-sm font-bold"
165
+ >
166
+ <CollapsibleTrigger className="!h-8">
167
+ {sectionLabel}
168
+ <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
169
+ </CollapsibleTrigger>
170
+ </SidebarGroupLabel>
171
+ <CollapsibleContent>
172
+ <SidebarGroupContent>
173
+ <SidebarMenu className="!gap-0.5">
174
+ <SidebarMenuSub className="!ml-2 !pl-1.5 !py-0 !gap-0.5">
175
+ {!hasNestedGroups &&
176
+ sectionItems.map((item) => (
177
+ <SidebarMenuSubItem key={item.shortId}>
178
+ <SidebarMenuSubButton
179
+ className="cursor-pointer !py-1.5"
180
+ data-taxonomy-id={item.taxonomyId}
181
+ data-taxonomy-short-id={item.shortId}
182
+ isActive={item.active}
183
+ onClick={() => onClickHandler(key, item)}
184
+ >
185
+ {item.label} ({item.hits}/{item.total})
186
+ {item.active && <Check className="ml-2" />}
187
+ </SidebarMenuSubButton>
188
+ </SidebarMenuSubItem>
189
+ ))}
190
+
191
+ {hasNestedGroups &&
192
+ Object.entries(
193
+ sectionItems.reduce<Record<string, ResolvedFilterItem[]>>((acc, item) => {
194
+ const groupLabel = item.groupLabel || "Other";
195
+ if (!acc[groupLabel]) acc[groupLabel] = [];
196
+ acc[groupLabel].push(item);
197
+ return acc;
198
+ }, {})
199
+ )
200
+ .sort(([a], [b]) => a.localeCompare(b))
201
+ .map(([groupLabel, groupedItems]) => (
202
+ <div key={groupLabel} className="pb-0.5">
203
+ <div className="px-2 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
204
+ {groupLabel}
205
+ </div>
206
+ {groupedItems.map((item) => (
207
+ <SidebarMenuSubItem key={item.shortId}>
208
+ <SidebarMenuSubButton
209
+ className="cursor-pointer !py-1.5"
210
+ data-taxonomy-id={item.taxonomyId}
211
+ data-taxonomy-short-id={item.shortId}
212
+ isActive={item.active}
213
+ onClick={() => onClickHandler(key, item)}
214
+ >
215
+ {item.label} ({item.hits}/{item.total})
216
+ {item.active && <Check className="ml-2" />}
217
+ </SidebarMenuSubButton>
218
+ </SidebarMenuSubItem>
219
+ ))}
220
+ </div>
221
+ ))}
222
+ </SidebarMenuSub>
223
+ </SidebarMenu>
224
+ </SidebarGroupContent>
225
+ </CollapsibleContent>
226
+ </SidebarGroup>
227
+ </Collapsible>
228
+ );
229
+ })}
230
+ </>
231
+ )}
232
+ </div>
233
+ );
234
+ })}
235
+ </SidebarContent>
236
+ )
237
+
238
+ /*
239
+ if (isMobile) {
240
+ return (
241
+
242
+ <SheetContent
243
+ side="left"
244
+ className="!pt-6 !px-2 w-[400px] overflow-y-auto"
245
+ >
246
+ <SheetHeader className="justify-center items-end font-bold">
247
+ {t("filter.filters")}
248
+ <span className="text-xs text-muted-foreground leading-5">
249
+ {totalItemCount} {t("results.results")}
250
+ </span>
251
+ </SheetHeader>
252
+ {content}
253
+ </SheetContent>
254
+ )
255
+ }
256
+ */
257
+ if (Object.entries(filteredTags).length === 0) return null;
258
+
259
+ return (
260
+ <div className="w-60 lg:w-80 bg-sidebar rounded-md border pb-4">
261
+ <SidebarHeader className="justify-center items-end font-bold">
262
+ {t("filter.filters")}
263
+ <span className="text-xs text-muted-foreground leading-5">
264
+ {totalItemCount} {t("results.results")}
265
+ </span>
266
+ </SidebarHeader>
267
+ {content}
268
+ </div>
269
+ );
270
+ };