@c-rex/components 0.3.0-build.35 → 0.3.0-build.38

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 (50) hide show
  1. package/package.json +32 -36
  2. package/src/article/article-action-bar.tsx +12 -2
  3. package/src/{check-article-lang.tsx → article/check-article-lang.tsx} +1 -1
  4. package/src/article/render-article-highlight.tsx +108 -0
  5. package/src/article/render-article.tsx +28 -0
  6. package/src/autocomplete.tsx +7 -25
  7. package/src/blog/blog-author-card.tsx +116 -0
  8. package/src/carousel/carousel.tsx +5 -2
  9. package/src/carousel/information-unit-carousel-item.tsx +1 -1
  10. package/src/content-unavailable.tsx +20 -0
  11. package/src/directoryNodes/directory-tree-context.tsx +9 -4
  12. package/src/documents/description-preview.tsx +14 -4
  13. package/src/documents/result-list-item.tsx +40 -46
  14. package/src/favorites/__tests__/favorites-hydration.test.tsx +245 -0
  15. package/src/favorites/bookmark-button.tsx +38 -20
  16. package/src/favorites/favorite-button.tsx +23 -24
  17. package/src/favorites/favorites-context.tsx +287 -0
  18. package/src/icons/file-icon.tsx +9 -26
  19. package/src/info/information-unit-metadata-grid-client.tsx +21 -21
  20. package/src/navbar/navbar.tsx +16 -30
  21. package/src/navbar/settings.tsx +1 -1
  22. package/src/page-wrapper.tsx +3 -3
  23. package/src/renditions/html-client.tsx +8 -6
  24. package/src/renditions/html.tsx +3 -1
  25. package/src/restriction-menu/restriction-menu-item.tsx +48 -58
  26. package/src/restriction-menu/restriction-selection-command-menu.tsx +445 -0
  27. package/src/restriction-menu/restriction-selection-menu.tsx +5 -7
  28. package/src/restriction-menu/taxonomy-restriction-command-menu.tsx +111 -0
  29. package/src/restriction-menu/taxonomy-restriction-menu.tsx +19 -12
  30. package/src/results/filter-navbar.tsx +81 -76
  31. package/src/results/filter-sidebar/context.tsx +32 -0
  32. package/src/results/filter-sidebar/index.tsx +40 -35
  33. package/src/results/generic/search-results-client.tsx +5 -4
  34. package/src/results/generic/table-result-list.tsx +16 -16
  35. package/src/results/information-unit-search-results-card-list.tsx +4 -1
  36. package/src/results/information-unit-search-results-cards.tsx +169 -69
  37. package/src/results/pagination.tsx +43 -40
  38. package/src/search-input.tsx +4 -2
  39. package/src/toc/toc-breadcrumb.tsx +1 -1
  40. package/src/toc/toc-browse-controls.tsx +2 -2
  41. package/src/toc/toc-tree-panel.tsx +19 -16
  42. package/src/article/article-content.tsx +0 -19
  43. package/src/breadcrumb.tsx +0 -124
  44. package/src/directoryNodes/tree-of-content.tsx +0 -68
  45. package/src/render-article.tsx +0 -75
  46. package/src/restriction-menu/restriction-menu-container.tsx +0 -4
  47. package/src/restriction-menu/restriction-menu.tsx +0 -4
  48. package/src/stores/__tests__/favorites-store.test.ts +0 -54
  49. package/src/stores/favorites-store.ts +0 -163
  50. /package/src/{render-article.module.css → article/render-article.module.css} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-rex/components",
3
- "version": "0.3.0-build.35",
3
+ "version": "0.3.0-build.38",
4
4
  "files": [
5
5
  "src"
6
6
  ],
@@ -21,10 +21,6 @@
21
21
  "types": "./src/generated/suggestions.tsx",
22
22
  "import": "./src/generated/suggestions.tsx"
23
23
  },
24
- "./breadcrumb": {
25
- "types": "./src/breadcrumb.tsx",
26
- "import": "./src/breadcrumb.tsx"
27
- },
28
24
  "./navbar": {
29
25
  "types": "./src/navbar/navbar.tsx",
30
26
  "import": "./src/navbar/navbar.tsx"
@@ -62,12 +58,12 @@
62
58
  "import": "./src/info/info-card.tsx"
63
59
  },
64
60
  "./check-article-lang": {
65
- "types": "./src/check-article-lang.tsx",
66
- "import": "./src/check-article-lang.tsx"
61
+ "types": "./src/article/check-article-lang.tsx",
62
+ "import": "./src/article/check-article-lang.tsx"
67
63
  },
68
64
  "./render-article": {
69
- "types": "./src/render-article.tsx",
70
- "import": "./src/render-article.tsx"
65
+ "types": "./src/article/render-article.tsx",
66
+ "import": "./src/article/render-article.tsx"
71
67
  },
72
68
  "./article-action-bar": {
73
69
  "types": "./src/article/article-action-bar.tsx",
@@ -98,17 +94,13 @@
98
94
  "import": "./src/renditions/html-client.tsx"
99
95
  },
100
96
  "./image-rendition-container": {
101
- "types": "./src/renditions/container.ts",
102
- "import": "./src/renditions/container.ts"
97
+ "types": "./src/renditions/image/container.tsx",
98
+ "import": "./src/renditions/image/container.tsx"
103
99
  },
104
100
  "./autocomplete": {
105
101
  "types": "./src/autocomplete.tsx",
106
102
  "import": "./src/autocomplete.tsx"
107
103
  },
108
- "./result-container": {
109
- "types": "./src/result-container.tsx",
110
- "import": "./src/result-container.tsx"
111
- },
112
104
  "./dialog-filter": {
113
105
  "types": "./src/results/dialog-filter.tsx",
114
106
  "import": "./src/results/dialog-filter.tsx"
@@ -117,6 +109,10 @@
117
109
  "types": "./src/results/pagination.tsx",
118
110
  "import": "./src/results/pagination.tsx"
119
111
  },
112
+ "./information-unit-search-results-card-list": {
113
+ "types": "./src/results/information-unit-search-results-card-list.tsx",
114
+ "import": "./src/results/information-unit-search-results-card-list.tsx"
115
+ },
120
116
  "./information-unit-search-results-cards": {
121
117
  "types": "./src/results/information-unit-search-results-cards.tsx",
122
118
  "import": "./src/results/information-unit-search-results-cards.tsx"
@@ -141,10 +137,10 @@
141
137
  "types": "./src/stores/highlight-store.ts",
142
138
  "import": "./src/stores/highlight-store.ts"
143
139
  },
144
- "./favorites-store": {
145
- "types": "./src/stores/favorites-store.ts",
146
- "import": "./src/stores/favorites-store.ts"
147
- },
140
+ "./favorites-context": {
141
+ "types": "./src/favorites/favorites-context.tsx",
142
+ "import": "./src/favorites/favorites-context.tsx"
143
+ },
148
144
  "./search-settings-store": {
149
145
  "types": "./src/stores/search-settings-store.ts",
150
146
  "import": "./src/stores/search-settings-store.ts"
@@ -177,14 +173,6 @@
177
173
  "types": "./src/renditions/image/rendition.tsx",
178
174
  "import": "./src/renditions/image/rendition.tsx"
179
175
  },
180
- "./server-image-rendition": {
181
- "types": "./src/renditions/image/server-image-rendition.tsx",
182
- "import": "./src/renditions/image/server-image-rendition.tsx"
183
- },
184
- "./article-skeleton": {
185
- "types": "./src/article/article-skeleton.tsx",
186
- "import": "./src/article/article-skeleton.tsx"
187
- },
188
176
  "./documents-result-list": {
189
177
  "types": "./src/documents/result-list.tsx",
190
178
  "import": "./src/documents/result-list.tsx"
@@ -201,14 +189,6 @@
201
189
  "types": "./src/carousel/carousel.tsx",
202
190
  "import": "./src/carousel/carousel.tsx"
203
191
  },
204
- "./restriction-menu": {
205
- "types": "./src/restriction-menu/restriction-menu.tsx",
206
- "import": "./src/restriction-menu/restriction-menu.tsx"
207
- },
208
- "./restriction-menu-container": {
209
- "types": "./src/restriction-menu/restriction-menu-container.tsx",
210
- "import": "./src/restriction-menu/restriction-menu-container.tsx"
211
- },
212
192
  "./restriction-menu-item": {
213
193
  "types": "./src/restriction-menu/restriction-menu-item.tsx",
214
194
  "import": "./src/restriction-menu/restriction-menu-item.tsx"
@@ -217,6 +197,10 @@
217
197
  "types": "./src/search-input.tsx",
218
198
  "import": "./src/search-input.tsx"
219
199
  },
200
+ "./content-unavailable": {
201
+ "types": "./src/content-unavailable.tsx",
202
+ "import": "./src/content-unavailable.tsx"
203
+ },
220
204
  "./footer": {
221
205
  "types": "./src/footer/footer.tsx",
222
206
  "import": "./src/footer/footer.tsx"
@@ -240,7 +224,19 @@
240
224
  "./taxonomy-restriction-menu": {
241
225
  "types": "./src/restriction-menu/taxonomy-restriction-menu.tsx",
242
226
  "import": "./src/restriction-menu/taxonomy-restriction-menu.tsx"
243
- }
227
+ },
228
+ "./restriction-selection-command-menu": {
229
+ "types": "./src/restriction-menu/restriction-selection-command-menu.tsx",
230
+ "import": "./src/restriction-menu/restriction-selection-command-menu.tsx"
231
+ },
232
+ "./taxonomy-restriction-command-menu": {
233
+ "types": "./src/restriction-menu/taxonomy-restriction-command-menu.tsx",
234
+ "import": "./src/restriction-menu/taxonomy-restriction-command-menu.tsx"
235
+ },
236
+ "./blog-author-card": {
237
+ "types": "./src/blog/blog-author-card.tsx",
238
+ "import": "./src/blog/blog-author-card.tsx"
239
+ }
244
240
  },
245
241
  "scripts": {
246
242
  "storybook": "storybook dev -p 6006",
@@ -10,12 +10,15 @@ import { cn } from "@c-rex/utils";
10
10
  import { useTranslations } from "next-intl";
11
11
  import { FavoriteButton } from "../favorites/favorite-button";
12
12
  import { ResultTypes } from "@c-rex/types";
13
+ import { ShareButton } from "../share-button";
13
14
  type Props = {
14
15
  id: string;
15
16
  articleType: ResultTypes;
16
17
  favoriteLabel: string;
18
+ className?: string;
19
+ children?: React.ReactNode;
17
20
  }
18
- export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel }) => {
21
+ export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel, className, children }) => {
19
22
  const t = useTranslations();
20
23
  const inputRef = useRef<HTMLInputElement>(null);
21
24
  const { next } = useHighlight();
@@ -31,7 +34,10 @@ export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel })
31
34
  }, [open]);
32
35
 
33
36
  return (
34
- <div className="w-9 flex gap-2 transition-all duration-300 flex-row z-20 items-end md:flex-col md:rounded-2xl md:sticky md:top-24 md:self-start">
37
+ <div className={cn(
38
+ "w-9 flex gap-2 transition-all duration-300 z-20 items-end flex-col sticky top-32 self-start",
39
+ className
40
+ )}>
35
41
 
36
42
  <SidebarTrigger side="right" />
37
43
 
@@ -41,6 +47,8 @@ export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel })
41
47
  label={favoriteLabel}
42
48
  />
43
49
 
50
+ <ShareButton />
51
+
44
52
  <Button
45
53
  variant="ghost"
46
54
  size="icon"
@@ -98,6 +106,8 @@ export const ArticleActionBar: FC<Props> = ({ id, articleType, favoriteLabel })
98
106
 
99
107
  </>
100
108
  )}
109
+
110
+ {children}
101
111
  </div>
102
112
  )
103
113
  }
@@ -5,7 +5,7 @@ import { useAppConfig } from "@c-rex/contexts/config-provider";
5
5
  import { AvailableVersionsInterface } from "@c-rex/interfaces";
6
6
  import { toast } from "sonner"
7
7
  import { useTranslations } from "next-intl"
8
- import { useSearchSettingsStore } from "./stores/search-settings-store";
8
+ import { useSearchSettingsStore } from "../stores/search-settings-store";
9
9
 
10
10
  interface Props {
11
11
  availableVersions: AvailableVersionsInterface[]
@@ -0,0 +1,108 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { useQueryState } from "nuqs";
5
+ import { useHighlight } from "@c-rex/contexts/highlight-provider";
6
+ import { useHighlightStore } from "../stores/highlight-store";
7
+
8
+ const EXCLUDED_TAGS = new Set(["MARK", "SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA"]);
9
+
10
+ export const extractHighlightTerms = (query: string | null): string[] => {
11
+ if (!query) return [];
12
+
13
+ return query
14
+ .split(/[+ ]/)
15
+ .map((term) => term.trim())
16
+ .filter(Boolean);
17
+ };
18
+
19
+ const applyHighlightToElement = (container: HTMLElement, terms: string[]) => {
20
+ if (terms.length === 0) return;
21
+
22
+ const escapedTerms = terms.map((term) =>
23
+ term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
24
+ );
25
+ const regex = new RegExp(`(${escapedTerms.join("|")})`, "gi");
26
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
27
+ const textNodes: Text[] = [];
28
+
29
+ let currentNode = walker.nextNode();
30
+ while (currentNode) {
31
+ const parent = currentNode.parentElement;
32
+ const textContent = currentNode.textContent ?? "";
33
+
34
+ if (
35
+ parent &&
36
+ !EXCLUDED_TAGS.has(parent.tagName) &&
37
+ textContent.trim().length > 0
38
+ ) {
39
+ const hasMatch = regex.test(textContent);
40
+ regex.lastIndex = 0;
41
+
42
+ if (hasMatch) {
43
+ textNodes.push(currentNode as Text);
44
+ }
45
+ }
46
+
47
+ currentNode = walker.nextNode();
48
+ }
49
+
50
+ textNodes.forEach((textNode) => {
51
+ const textContent = textNode.textContent ?? "";
52
+ const parts = textContent.split(regex);
53
+
54
+ if (parts.length <= 1) return;
55
+
56
+ const fragment = document.createDocumentFragment();
57
+
58
+ parts.forEach((part) => {
59
+ if (!part) return;
60
+
61
+ const isMatch = regex.test(part);
62
+ regex.lastIndex = 0;
63
+
64
+ if (isMatch) {
65
+ const mark = document.createElement("mark");
66
+ mark.className = "bg-yellow-200";
67
+ mark.textContent = part;
68
+ fragment.appendChild(mark);
69
+ return;
70
+ }
71
+
72
+ fragment.appendChild(document.createTextNode(part));
73
+ });
74
+
75
+ textNode.parentNode?.replaceChild(fragment, textNode);
76
+ });
77
+ };
78
+
79
+ type RenderArticleHighlightProps = {
80
+ containerId: string
81
+ htmlContent: string
82
+ };
83
+
84
+ export const RenderArticleHighlight = ({
85
+ containerId,
86
+ htmlContent,
87
+ }: RenderArticleHighlightProps) => {
88
+ const [query] = useQueryState("q");
89
+ const enableHighlight = useHighlightStore((state) => state.enable);
90
+ const { registerContainer, refreshMarks } = useHighlight();
91
+
92
+ useEffect(() => {
93
+ const container = document.getElementById(containerId) as HTMLElement | null;
94
+ if (!container) return;
95
+
96
+ registerContainer(container);
97
+ container.innerHTML = htmlContent;
98
+
99
+ if (enableHighlight) {
100
+ const terms = extractHighlightTerms(query);
101
+ applyHighlightToElement(container, terms);
102
+ }
103
+
104
+ refreshMarks();
105
+ }, [containerId, enableHighlight, htmlContent, query, refreshMarks, registerContainer]);
106
+
107
+ return null;
108
+ };
@@ -0,0 +1,28 @@
1
+ import { useId } from "react";
2
+ import styles from "./render-article.module.css";
3
+ import { RenderArticleHighlight } from "./render-article-highlight";
4
+
5
+ type Props = {
6
+ htmlContent: string;
7
+ contentLang?: string;
8
+ };
9
+
10
+ export const RenderArticle = ({ htmlContent, contentLang }: Props) => {
11
+ const containerId = useId().replace(/:/g, "");
12
+
13
+ return (
14
+ <>
15
+ <main
16
+ id={containerId}
17
+ data-content-scope="dita"
18
+ lang={contentLang}
19
+ className={`ids-content ids-content--dita-ot ${styles.idsContent}`}
20
+ dangerouslySetInnerHTML={{ __html: htmlContent }}
21
+ />
22
+ <RenderArticleHighlight
23
+ containerId={containerId}
24
+ htmlContent={htmlContent}
25
+ />
26
+ </>
27
+ );
28
+ };
@@ -101,27 +101,6 @@ export const AutoComplete = ({
101
101
  });
102
102
  };
103
103
 
104
- const clearSearch = () => {
105
- setQuery("");
106
- setOpen(false);
107
- const nextParams = new URLSearchParams(keepParams ? searchParams.toString() : "");
108
- nextParams.set("page", "1");
109
- nextParams.delete("search");
110
- const queryString = nextParams.toString();
111
-
112
- onSelectParams?.forEach(param => {
113
- nextParams.set(param.key, param.value);
114
- });
115
- const targetUrl = queryString ? `${onSelectPath}?${queryString}` : onSelectPath;
116
- const currentUrl = `${pathname}${searchParams.toString().length > 0 ? `?${searchParams.toString()}` : ""}`;
117
- if (targetUrl === currentUrl) return;
118
-
119
- startSearchNavigation();
120
- startNavigation(() => {
121
- router.push(targetUrl);
122
- });
123
- };
124
-
125
104
  useEffect(() => setQuery(initialValue), [initialValue]);
126
105
  useEffect(() => {
127
106
  stopSearchNavigation();
@@ -199,6 +178,7 @@ export const AutoComplete = ({
199
178
  setQuery(e.target.value);
200
179
  setOpen(true);
201
180
  }}
181
+ onFocus={() => setOpen(true)}
202
182
  />
203
183
  {isNavigating || searchNavigationPending ? (
204
184
  <InputGroupAddon align="inline-end">
@@ -209,8 +189,8 @@ export const AutoComplete = ({
209
189
  <InputGroupButton
210
190
  size="icon-xs"
211
191
  variant="ghost"
212
- aria-label="Clear search"
213
- onClick={clearSearch}
192
+ aria-label={t("clearSearch")}
193
+ onClick={() => setQuery("")}
214
194
  >
215
195
  <X className="size-3" />
216
196
  </InputGroupButton>
@@ -221,7 +201,9 @@ export const AutoComplete = ({
221
201
 
222
202
  {open && (
223
203
  <ul className="suggestions-list absolute z-10 w-full bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
224
- {loading ? (
204
+ {query.length === 0 ? (
205
+ <li className="px-4 py-2 text-sm text-muted-foreground">{t("typingHint")}</li>
206
+ ) : loading ? (
225
207
  <li>
226
208
  <div className="flex items-center justify-center py-4">
227
209
  <div className="animate-spin rounded-full h-6 w-6 border-2 border-gray-300 border-t-gray-950" />
@@ -246,7 +228,7 @@ export const AutoComplete = ({
246
228
  ))}
247
229
  </>
248
230
  ) : (
249
- <li className="px-4 py-2">No suggestions.</li>
231
+ <li className="px-4 py-2">{t("noSuggestions")}</li>
250
232
  )}
251
233
  </>
252
234
  )}
@@ -0,0 +1,116 @@
1
+ import { User, Mail, Link as LinkIcon } from "lucide-react";
2
+ import Link from "next/link";
3
+ import { cn } from "@c-rex/utils";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@c-rex/ui/card";
5
+
6
+ export type BlogAuthor = {
7
+ name: string;
8
+ photo: string | null;
9
+ title?: string | null;
10
+ role?: string | null;
11
+ emails?: string[];
12
+ urls?: string[];
13
+ };
14
+
15
+ type Props = {
16
+ authors: BlogAuthor[];
17
+ label: string;
18
+ embedded?: boolean;
19
+ fields?: Array<keyof BlogAuthor>;
20
+ className?: string;
21
+ };
22
+
23
+ const DEFAULT_FIELDS: Array<keyof BlogAuthor> = ["photo", "name", "title", "role"];
24
+
25
+ const AuthorAvatar = ({ author }: { author: BlogAuthor }) => (
26
+ author.photo ? (
27
+ <img
28
+ src={author.photo}
29
+ alt={author.name}
30
+ className="size-10 rounded-full object-cover shrink-0"
31
+ />
32
+ ) : (
33
+ <div className="size-10 rounded-full bg-muted shrink-0 flex items-center justify-center">
34
+ <User className="size-6" />
35
+ </div>
36
+ )
37
+ );
38
+
39
+ const AuthorEntry = ({ author, fields }: { author: BlogAuthor; fields: Array<keyof BlogAuthor> }) => {
40
+ const showPhoto = fields.includes("photo");
41
+ const showName = fields.includes("name");
42
+ const showTitle = fields.includes("title");
43
+ const showRole = fields.includes("role");
44
+ const showEmails = fields.includes("emails");
45
+ const showUrls = fields.includes("urls");
46
+
47
+ return (
48
+ <div className="flex items-start gap-3">
49
+ {showPhoto && <AuthorAvatar author={author} />}
50
+ <div className="flex flex-col gap-0.5 min-w-0">
51
+ {showName && (
52
+ <span className="text-sm font-medium truncate">{author.name}</span>
53
+ )}
54
+ {showTitle && author.title && (
55
+ <span className="text-xs text-muted-foreground truncate">{author.title}</span>
56
+ )}
57
+ {showRole && author.role && (
58
+ <span className="text-xs text-muted-foreground truncate">{author.role}</span>
59
+ )}
60
+ {showEmails && author.emails && author.emails.length > 0 && (
61
+ <div className="flex flex-col gap-0.5 mt-1">
62
+ {author.emails.map((email) => (
63
+ <Link
64
+ key={email}
65
+ href={`mailto:${email}`}
66
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
67
+ >
68
+ <Mail className="size-3 shrink-0" />
69
+ <span className="truncate">{email}</span>
70
+ </Link>
71
+ ))}
72
+ </div>
73
+ )}
74
+ {showUrls && author.urls && author.urls.length > 0 && (
75
+ <div className="flex flex-col gap-0.5 mt-1">
76
+ {author.urls.map((url) => (
77
+ <Link
78
+ key={url}
79
+ href={url}
80
+ target="_blank"
81
+ rel="noopener noreferrer"
82
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
83
+ >
84
+ <LinkIcon className="size-3 shrink-0" />
85
+ <span className="truncate">{url}</span>
86
+ </Link>
87
+ ))}
88
+ </div>
89
+ )}
90
+ </div>
91
+ </div>
92
+ );
93
+ };
94
+
95
+ export const BlogAuthorCard = ({ authors, label, embedded = false, fields = DEFAULT_FIELDS, className }: Props) => {
96
+ if (authors.length === 0) return null;
97
+
98
+ const content = (
99
+ <CardContent className="space-y-3 pb-4">
100
+ {authors.map((author, i) => (
101
+ <AuthorEntry key={i} author={author} fields={fields} />
102
+ ))}
103
+ </CardContent>
104
+ );
105
+
106
+ if (embedded) return content;
107
+
108
+ return (
109
+ <Card className={cn("gap-0", className)}>
110
+ <CardHeader>
111
+ <CardTitle className="text-lg">{label}</CardTitle>
112
+ </CardHeader>
113
+ {content}
114
+ </Card>
115
+ );
116
+ };
@@ -298,6 +298,7 @@ export const Carousel: FC<Props> = ({
298
298
  <div ref={containerRef} className={cn("w-full flex items-center")}>
299
299
  {(arrows && data && data.length > 0) && <CarouselPrev />}
300
300
  <div className={cn("flex-1 overflow-hidden relative flex items-center")}>
301
+ {/*TODO: update loading position*/}
301
302
  {loadByPages && loading && data && data.length > 0 && (
302
303
  <div className="absolute right-3 top-3 z-20 rounded-full bg-background/95 p-2 text-muted-foreground shadow-sm">
303
304
  <LoaderCircle className="h-3.5 w-3.5 animate-spin" />
@@ -327,7 +328,10 @@ export const Carousel: FC<Props> = ({
327
328
  data.map((item: CommonItemsModel, index: number) => (
328
329
  <div
329
330
  key={item.shortId}
330
- className={`flex-shrink-0 flex-grow-0 flex justify-center`}
331
+ className={cn(
332
+ 'flex-shrink-0 flex-grow-0 flex justify-center',
333
+ loading && "opacity-40"
334
+ )}
331
335
  style={{ width: `${100 / slidesToShow}%` }}
332
336
  >
333
337
  <RenderComponent
@@ -454,7 +458,6 @@ const CarouselIndicators: FC<{ className?: string }> = ({ className }) => {
454
458
  );
455
459
  })}
456
460
  </ol>
457
- {pagedMode && loading && <LoaderCircle className="h-3.5 w-3.5 animate-spin text-muted-foreground" />}
458
461
  </div>
459
462
  );
460
463
  };
@@ -65,7 +65,7 @@ export const InformationUnitCarouselItem: FC<Props> = ({
65
65
  <Tooltip>
66
66
  <TooltipTrigger asChild>
67
67
  <div className="h-44 overflow-hidden">
68
- <span className="line-clamp-5 text-lg font-semibold group-hover:underline">
68
+ <span className="line-clamp-5 text-lg font-semibold group-hover:underline max-w-full overflow-ellipsis [overflow-wrap:anywhere] hyphens-auto">
69
69
  {title}
70
70
  </span>
71
71
  </div>
@@ -0,0 +1,20 @@
1
+ import { ComponentProps, FC } from "react";
2
+ import { PageWrapper } from "@c-rex/components/page-wrapper";
3
+ import { getTranslations } from "next-intl/server";
4
+
5
+
6
+ type Props = Omit<ComponentProps<typeof PageWrapper>, "children">;
7
+
8
+ export const ContentUnavailable: FC<Props> = async ({ ...props }) => {
9
+ const t = await getTranslations();
10
+
11
+ return (
12
+ <PageWrapper {...props}>
13
+ <div className="flex flex-1 justify-center items-center p-4">
14
+ <div className="rounded-md border p-4 text-sm text-muted-foreground">
15
+ {t("contentTemporarilyUnavailableRetry")}
16
+ </div>
17
+ </div>
18
+ </PageWrapper>
19
+ )
20
+ }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { createContext, useContext, useEffect, useMemo, useState, type FC, type PropsWithChildren } from "react";
4
+ import { useTranslations } from "next-intl";
4
5
  import { useRouter } from "next/navigation";
5
6
  import type { TreeOfContent } from "@c-rex/interfaces";
6
7
  import {
@@ -232,6 +233,7 @@ type DirectoryTreeSidebarMenuProps = {
232
233
  export const DirectoryTreeSidebarMenu: FC<DirectoryTreeSidebarMenuProps> = ({
233
234
  onNavigateNode,
234
235
  }) => {
236
+ const t = useTranslations();
235
237
  const { tree, isLoading, expandActivePathOnLoad } = useDirectoryTree();
236
238
  const [expandedNodeIds, setExpandedNodeIds] = useState<Set<string>>(new Set());
237
239
 
@@ -266,7 +268,8 @@ export const DirectoryTreeSidebarMenu: FC<DirectoryTreeSidebarMenuProps> = ({
266
268
  <Skeleton className="w-auto h-10 mb-2 ml-8" />
267
269
  <Skeleton className="w-auto h-10 mb-2 ml-8" />
268
270
  <Skeleton className="w-auto h-10 mb-2" />
269
- <div className="px-2 pt-1 text-xs text-muted-foreground">Inhaltsverzeichnis wird geladen...</div>
271
+ {/*TODO: need this?*/}
272
+ <div className="px-2 pt-1 text-xs text-muted-foreground">{t("loadingTableOfContents")}</div>
270
273
  </div>
271
274
  );
272
275
  }
@@ -302,7 +305,7 @@ export const DirectoryTreeSidebarMenu: FC<DirectoryTreeSidebarMenuProps> = ({
302
305
  {node.children?.length ? (
303
306
  <button
304
307
  type="button"
305
- aria-label={expandedNodeIds.has(node.id) ? "Collapse section" : "Expand section"}
308
+ aria-label={expandedNodeIds.has(node.id) ? t("collapseSection") : t("expandSection")}
306
309
  className="h-8 w-8 inline-flex items-center justify-center text-muted-foreground hover:text-foreground"
307
310
  onClick={() => toggleNode(node.id)}
308
311
  >
@@ -335,11 +338,13 @@ type DirectoryTreeBreadcrumbProps = {
335
338
 
336
339
  export const DirectoryTreeBreadcrumb: FC<DirectoryTreeBreadcrumbProps> = ({
337
340
  lang,
338
- homeLabel = "Home",
341
+ homeLabel,
339
342
  onNavigateHome,
340
343
  onNavigateNodeId,
341
344
  }) => {
345
+ const t = useTranslations();
342
346
  const { tree, isLoading } = useDirectoryTree();
347
+ const resolvedHomeLabel = homeLabel ?? t("home");
343
348
  const router = useRouter();
344
349
  const items = generateBreadcrumbItems(tree);
345
350
 
@@ -362,7 +367,7 @@ export const DirectoryTreeBreadcrumb: FC<DirectoryTreeBreadcrumbProps> = ({
362
367
  <BreadcrumbList>
363
368
  <BreadcrumbItem>
364
369
  <button type="button" onClick={navigateHome}>
365
- {homeLabel}
370
+ {resolvedHomeLabel}
366
371
  </button>
367
372
  </BreadcrumbItem>
368
373
  <BreadcrumbSeparator />