@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
@@ -1,4 +1,5 @@
1
1
  import { FC } from "react";
2
+ import { useTranslations } from "next-intl";
2
3
  import { CommonItemsModel } from "@c-rex/interfaces";
3
4
  import { FileStack } from "lucide-react";
4
5
  import { FileDownloadDropdown } from "../renditions/file-download";
@@ -32,6 +33,8 @@ export const InformationUnitSearchResultsCardList: FC<InformationUnitSearchResul
32
33
  imageFragmentSubjectIds = [],
33
34
  descriptionFragmentSubjectIds = [],
34
35
  }) => {
36
+ const t = useTranslations();
37
+
35
38
  return (
36
39
  <div className="flex-1">
37
40
  {items.map((item, index) => {
@@ -115,7 +118,7 @@ export const InformationUnitSearchResultsCardList: FC<InformationUnitSearchResul
115
118
  </Button>
116
119
  </TooltipTrigger>
117
120
  <TooltipContent>
118
- Available in: {multipleVersions.join(", ")}
121
+ {t("availableIn")}: {multipleVersions.join(", ")}
119
122
  </TooltipContent>
120
123
  </Tooltip>
121
124
  )}
@@ -1,96 +1,196 @@
1
1
  "use client"
2
2
 
3
- import { FC, useState } from "react";
3
+ import { FC, useEffect, useRef, useState } from "react";
4
4
 
5
5
  import Link from "next/link";
6
- import { cn } from "@c-rex/utils";
6
+ import { cn, formatDateToLocale } from "@c-rex/utils";
7
7
  import { Badge } from "@c-rex/ui/badge";
8
- import { useTranslations } from "next-intl";
8
+ import { useLocale } from "next-intl";
9
9
  import { TopicsResponseItem } from "@c-rex/interfaces";
10
10
  import { Card } from "@c-rex/ui/card";
11
11
  import { useQueryState } from "nuqs";
12
+ import { User } from "lucide-react";
12
13
 
13
14
  interface InformationUnitSearchResultsCardsProps {
14
15
  items: TopicsResponseItem[];
15
16
  }
16
17
 
17
- const InformationUnitSearchResultsCards: FC<InformationUnitSearchResultsCardsProps> = ({ items }) => {
18
- const t = useTranslations("results")
19
- const [isLoading, setLoading] = useState(true);
20
- const [query] = useQueryState("search");
18
+ type CardData = {
19
+ image: string | null;
20
+ description: string | null;
21
+ authors: { name: string; photo: string | null }[];
22
+ loaded: boolean;
23
+ };
21
24
 
22
- return (
23
- <div className="grid gap-6 grid-cols-2">
24
25
 
25
- {items.map((item, index) => (
26
- <Card
27
- key={item.shortId}
28
- className={cn(
29
- `c-rex-result-item information-unit-search-results-card relative p-0 c-rex-result-${item.type.toLowerCase()}`,
30
- index == 0
31
- ? "col-span-2 grid grid-cols-1 gap-3 md:grid-cols-2 md:gap-6"
32
- : "flex flex-col space-y-2",
33
- item.disabled ? "c-rex-result-item-disabled" : ""
26
+ const InformationUnitSearchResultCard: FC<{
27
+ item: TopicsResponseItem;
28
+ index: number;
29
+ query: string | null;
30
+ }> = ({ item, index, query }) => {
31
+ const locale = useLocale();
32
+ const date = formatDateToLocale(item.created!, locale);
33
+ const cardRef = useRef<HTMLDivElement>(null);
34
+ const [cardData, setCardData] = useState<CardData>({ image: null, description: null, authors: [], loaded: false });
35
+
36
+ useEffect(() => {
37
+ const el = cardRef.current;
38
+ if (!el) return;
39
+
40
+ const observer = new IntersectionObserver(
41
+ ([entry]) => {
42
+ if (!entry.isIntersecting) return;
43
+ observer.disconnect();
44
+
45
+ const fetches: Promise<void>[] = [];
46
+
47
+ if (item.renditionUrl) {
48
+ fetches.push(
49
+ fetch(item.renditionUrl)
50
+ .then(r => r.text())
51
+ .then(html => {
52
+ const doc = new DOMParser().parseFromString(html, "text/html");
53
+ setCardData(prev => ({
54
+ ...prev,
55
+ image: doc.querySelector(".teaserfig img")?.getAttribute("src") || null,
56
+ description: doc.querySelector(".teaser-p")?.textContent || null,
57
+ }));
58
+ })
59
+ .catch(() => { })
60
+ );
61
+ }
62
+
63
+ if (item.vcardUrls?.length) {
64
+ fetches.push(
65
+ Promise.all(
66
+ item.vcardUrls.map(url =>
67
+ fetch(url)
68
+ .then(r => r.json())
69
+ .then(vcard => ({ name: vcard.fullName || "", photo: vcard.photos?.[0]?.source || null }))
70
+ .catch(() => null)
71
+ )
72
+ ).then(results => {
73
+ setCardData(prev => ({
74
+ ...prev,
75
+ authors: results.filter(Boolean) as { name: string; photo: string | null }[],
76
+ }));
77
+ })
78
+ );
79
+ }
80
+
81
+ Promise.all(fetches).then(() => {
82
+ setCardData(prev => ({ ...prev, loaded: true }));
83
+ });
84
+ },
85
+ { rootMargin: "200px" }
86
+ );
87
+
88
+ observer.observe(el);
89
+ return () => observer.disconnect();
90
+ }, [item.renditionUrl, item.vcardUrls]);
91
+
92
+ return (
93
+ <div ref={cardRef} className={cn(index == 0 ? "md:col-span-2" : "")}>
94
+ <Card
95
+ className={cn(
96
+ "c-rex-result-item information-unit-search-results-card relative p-0 gap-0",
97
+ `c-rex-result-${item.type.toLowerCase()}`,
98
+ index == 0
99
+ ? "grid grid-cols-1 md:grid-cols-2"
100
+ : "h-full flex flex-col space-y-2",
101
+ item.disabled ? "c-rex-result-item-disabled" : ""
102
+ )}
103
+ >
104
+ <div className={cn(
105
+ "w-full overflow-hidden",
106
+ index == 0 ? "rounded-t-xl md:rounded-t-none md:rounded-tl-xl md:rounded-bl-xl" : "rounded-t-xl"
107
+ )}>
108
+ {cardData.image ? (
109
+ <img
110
+ src={cardData.image}
111
+ alt={item.title}
112
+ className="size-full object-cover object-center"
113
+ style={{ width: "100%", height: index == 0 ? "100%" : "190px" }}
114
+ loading={index == 0 ? "eager" : "lazy"}
115
+ />
116
+ ) : (
117
+ <div
118
+ style={{ height: "190px" }}
119
+ className={cn(
120
+ "w-full bg-gray-100 animate-pulse",
121
+ index == 0 ? "" : "rounded-t-xl"
122
+ )}
123
+ />
34
124
  )}
35
- >
36
-
37
- <div className={
38
- cn(
39
- "w-full overflow-hidden",
40
- index == 0 ? "rounded-tl-xl rounded-bl-xl" : "rounded-t-xl"
41
- )}>
42
- {item.image && (
43
- <img
44
- src={item.image}
45
- alt={item.title}
46
- className={cn(
47
- "size-full object-cover object-center",
48
- isLoading ? "bg-gray-300 animate-pulse" : "",
49
- )}
50
- style={{
51
- width: "100%", height: index == 0 ? "auto" : "190px"
52
- }}
53
- loading={index == 0 ? "eager" : "lazy"}
54
- onLoad={() => setLoading(false)}
55
- onError={() => setLoading(false)}
56
- />
57
- )}
125
+ </div>
58
126
 
127
+ <div className="flex flex-1 flex-col p-4 gap-4 justify-between">
128
+ <div className="w-full flex flex-col items-start gap-4">
129
+ <h2 className="line-clamp-2 font-heading text-2xl">
130
+ {item.title}
131
+ </h2>
132
+ <Badge variant="secondary"> {date} </Badge>
59
133
  </div>
60
134
 
61
- <div
62
- className={cn(
63
- "flex flex-1 flex-col p-4",
64
- index == 0 ? "justify-around" : "justify-between",
135
+ <div className="flex items-end flex-row">
136
+ {cardData.description ? (
137
+ <p className="m-0 flex-1 text-sm text-muted-foreground">
138
+ {cardData.description}
139
+ </p>
140
+ ) : (
141
+ <div className="flex-1 space-y-2">
142
+ <div className="h-3 w-full rounded bg-gray-100 animate-pulse" />
143
+ <div className="h-3 w-4/5 rounded bg-gray-100 animate-pulse" />
144
+ </div>
65
145
  )}
66
- >
67
- <div className="w-full">
68
- <h2 className="my-1.5 line-clamp-2 font-heading text-2xl">
69
- {item.title}
70
- </h2>
71
-
72
- {item.type && (
73
- <Badge variant="secondary">
74
- {item.type}
75
- </Badge>
76
- )}
77
- </div>
78
- <div className="mt-4 flex items-end flex-row gap-2">
79
- <p className="m-0 flex-1 text-sm text-muted-foreground">{item.description?.substring(0, 100)}...</p>
146
+ </div>
80
147
 
81
- {item.created && (
82
- <p className="m-0 text-sm text-muted-foreground">{item.created}</p>
83
- )}
148
+ {cardData.authors.length > 0 && (
149
+ <div className="flex flex-wrap gap-4">
150
+ {cardData.authors.map((author, i) => (
151
+ <div key={i} className="flex items-center gap-2">
84
152
 
85
- </div>
86
- </div>
87
153
 
88
- {!item.disabled && (
89
- <Link href={`${item.link}?q=${query}`} className="absolute inset-0">
90
- <span className="sr-only">{t("viewArticle")}</span>
91
- </Link>
154
+ {author.photo ? (
155
+ <img
156
+ src={author.photo}
157
+ alt={author.name}
158
+ className="size-6 rounded-full object-cover"
159
+ />
160
+ ) : (
161
+ <div className="size-6 rounded-full bg-muted flex items-center justify-center" >
162
+ <User className="size-4" />
163
+ </div>
164
+ )}
165
+ <span className="text-xs text-muted-foreground">{author.name}</span>
166
+ </div>
167
+ ))}
168
+ </div>
92
169
  )}
93
- </Card>
170
+ </div>
171
+
172
+ {!item.disabled && (
173
+ <Link href={item.link} className="absolute inset-0">
174
+ <span className="sr-only">View article</span>
175
+ </Link>
176
+ )}
177
+ </Card>
178
+ </div>
179
+ );
180
+ };
181
+
182
+ const InformationUnitSearchResultsCards: FC<InformationUnitSearchResultsCardsProps> = ({ items }) => {
183
+ const [query] = useQueryState("search");
184
+
185
+ return (
186
+ <div className="grid gap-6 md:grid-cols-2">
187
+ {items.map((item, index) => (
188
+ <InformationUnitSearchResultCard
189
+ key={item.shortId}
190
+ item={item}
191
+ index={index}
192
+ query={query}
193
+ />
94
194
  ))}
95
195
  </div>
96
196
  );
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import { FC, MouseEvent } from "react";
3
+ import { FC } from "react";
4
4
  import {
5
5
  Pagination as PaginationUI,
6
6
  PaginationContent,
@@ -10,7 +10,7 @@ import {
10
10
  PaginationNext,
11
11
  PaginationPrevious,
12
12
  } from "@c-rex/ui/pagination"
13
- import { parseAsInteger, useQueryState } from "nuqs";
13
+ import { usePathname, useSearchParams } from "next/navigation";
14
14
  import { ResultContainerPageInfoModel } from "@c-rex/interfaces";
15
15
  import { useTranslations } from "next-intl";
16
16
  import { cn } from "@c-rex/utils";
@@ -22,6 +22,30 @@ interface PaginationProps {
22
22
 
23
23
  type PaginationToken = number | "ellipsis-left" | "ellipsis-right";
24
24
 
25
+ const isValidPageNumber = (pageNumber: number, currentPage: number, pageCount: number) => (
26
+ pageNumber >= 1
27
+ && pageNumber <= pageCount
28
+ && pageNumber !== currentPage
29
+ );
30
+
31
+ export const buildPageHref = (
32
+ pathname: string,
33
+ currentSearch: string,
34
+ pageNumber: number,
35
+ currentPage: number,
36
+ pageCount: number,
37
+ ) => {
38
+ if (!isValidPageNumber(pageNumber, currentPage, pageCount)) {
39
+ return `${pathname}${currentSearch}`;
40
+ }
41
+
42
+ const searchParams = new URLSearchParams(currentSearch);
43
+ searchParams.set("page", String(pageNumber));
44
+
45
+ const queryString = searchParams.toString();
46
+ return queryString.length > 0 ? `${pathname}?${queryString}` : pathname;
47
+ };
48
+
25
49
  const buildPaginationTokens = (currentPage: number, pageCount: number): PaginationToken[] => {
26
50
  if (pageCount <= 7) {
27
51
  return Array.from({ length: pageCount }, (_, index) => index + 1);
@@ -41,27 +65,8 @@ const buildPaginationTokens = (currentPage: number, pageCount: number): Paginati
41
65
  export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
42
66
  const disabledClass = "opacity-50 pointer-events-none";
43
67
  const t = useTranslations("results");
44
-
45
- const [, setPage] = useQueryState('page',
46
- parseAsInteger.withOptions({
47
- history: 'push',
48
- shallow: false,
49
- })
50
- )
51
-
52
- const onChangePage = (event: MouseEvent<HTMLAnchorElement>, pageNumber: number) => {
53
- event.preventDefault();
54
-
55
- if (
56
- pageNumber < 1
57
- || (pageInfo.pageCount !== undefined && pageNumber > pageInfo.pageCount)
58
- || pageNumber === pageInfo.pageNumber
59
- ) {
60
- return;
61
- }
62
-
63
- void setPage(pageNumber);
64
- }
68
+ const pathname = usePathname();
69
+ const searchParams = useSearchParams();
65
70
 
66
71
  const pageNumber = pageInfo.pageNumber || 1;
67
72
  const pageCount = pageInfo.pageCount || 1;
@@ -69,32 +74,25 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
69
74
  const firstItemOnPage = pageInfo.firstItemOnPage || 0;
70
75
  const lastItemOnPage = pageInfo.lastItemOnPage || 0;
71
76
  const tokens = buildPaginationTokens(pageNumber, pageCount);
77
+ const currentSearch = searchParams.toString();
78
+ const search = currentSearch.length > 0 ? `?${currentSearch}` : "";
72
79
 
73
80
  return (
74
81
  <div className={cn("flex flex-col gap-3 py-4", className)}>
75
- <div className="flex flex-col gap-1 text-sm text-muted-foreground md:flex-row md:items-center md:justify-between">
76
- <span>
82
+ <PaginationUI className="py-4 items-center justify-center sm:justify-between">
83
+ <span className="hidden sm:block text-sm text-muted-foreground">
77
84
  {t("paginationResults", {
78
85
  first: firstItemOnPage,
79
86
  last: lastItemOnPage,
80
87
  total: totalItemCount,
81
88
  })}
82
89
  </span>
83
- <span>
84
- {t("paginationPage", {
85
- page: pageNumber,
86
- pageCount,
87
- })}
88
- </span>
89
- </div>
90
90
 
91
- <PaginationUI className="py-0">
92
- <PaginationContent className="justify-start">
91
+ <PaginationContent className="justify-between sm:justify-start w-full sm:w-auto">
93
92
  <PaginationItem>
94
93
  <PaginationPrevious
95
- href="#"
94
+ href={buildPageHref(pathname, search, pageNumber - 1, pageNumber, pageCount)}
96
95
  className={pageNumber === 1 ? disabledClass : ""}
97
- onClick={(event) => onChangePage(event, pageNumber - 1)}
98
96
  />
99
97
  </PaginationItem>
100
98
 
@@ -102,9 +100,8 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
102
100
  typeof token === "number" ? (
103
101
  <PaginationItem key={token}>
104
102
  <PaginationLink
105
- href="#"
103
+ href={buildPageHref(pathname, search, token, pageNumber, pageCount)}
106
104
  isActive={token === pageNumber}
107
- onClick={(event) => onChangePage(event, token)}
108
105
  >
109
106
  {token}
110
107
  </PaginationLink>
@@ -118,12 +115,18 @@ export const Pagination: FC<PaginationProps> = ({ pageInfo, className }) => {
118
115
 
119
116
  <PaginationItem>
120
117
  <PaginationNext
121
- href="#"
122
- onClick={(event) => onChangePage(event, pageNumber + 1)}
118
+ href={buildPageHref(pathname, search, pageNumber + 1, pageNumber, pageCount)}
123
119
  className={pageNumber === pageCount ? disabledClass : ""}
124
120
  />
125
121
  </PaginationItem>
126
122
  </PaginationContent>
123
+
124
+ <span className="hidden sm:block text-sm text-muted-foreground">
125
+ {t("paginationPage", {
126
+ page: pageNumber,
127
+ pageCount,
128
+ })}
129
+ </span>
127
130
  </PaginationUI>
128
131
  </div>
129
132
  )
@@ -5,6 +5,7 @@ import { FileCheck, FileX, Search } from "lucide-react";
5
5
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@c-rex/ui/tooltip";
6
6
  import * as AutocompleteOptions from "./generated/suggestions";
7
7
  import { useQueryState } from "nuqs";
8
+ import { useTranslations } from "next-intl";
8
9
 
9
10
  type PlacedOn = "NAVBAR" | "BODY"
10
11
  type Props = {
@@ -24,6 +25,7 @@ export const SearchInput: FC<Props> = ({
24
25
  alternativeAutocompleteType,
25
26
  alternativeOnSelectPath
26
27
  }) => {
28
+ const t = useTranslations();
27
29
  const [pkg] = useQueryState("package");
28
30
  const [checked, setChecked] = useState<boolean>(true);
29
31
  const [autocompleteComponentName, setAutocompleteComponentName] = useState<keyof typeof AutocompleteOptions>(autocompleteType);
@@ -61,11 +63,11 @@ export const SearchInput: FC<Props> = ({
61
63
  </TooltipTrigger>
62
64
 
63
65
  <TooltipContent>
64
- If checked will search only in this document
66
+ {t("searchInDocument")}
65
67
  </TooltipContent>
66
68
  </Tooltip>
67
69
  </TooltipProvider>
68
70
  </>}
69
71
  </>
70
72
  );
71
- };
73
+ };
@@ -9,7 +9,7 @@ import {
9
9
  } from "@c-rex/ui/breadcrumb";
10
10
  import type { TocBreadcrumbItem } from "./types";
11
11
 
12
- type TocBreadcrumbProps = {
12
+ export type TocBreadcrumbProps = {
13
13
  lang: string;
14
14
  homeLabel: string;
15
15
  homeHref?: string;
@@ -46,7 +46,7 @@ export const TocBrowseControls = ({
46
46
  >
47
47
  <div className={cn("min-w-0", justify === "between" ? "flex-1" : "flex-none")}>
48
48
  {previous ? (
49
- <Button asChild variant="ghost" className={cn("max-w-full justify-start px-0", buttonClassName)}>
49
+ <Button asChild variant="ghost" size="icon" className={buttonClassName}>
50
50
  <Link href={previous.href} title={previous.label} className={cn("inline-flex min-w-0 items-center", buttonGapClassName)}>
51
51
  <ChevronLeft className="h-4 w-4 shrink-0" />
52
52
  {showLabels ? <span className="truncate">{previous.label}</span> : null}
@@ -59,7 +59,7 @@ export const TocBrowseControls = ({
59
59
 
60
60
  <div className={cn("min-w-0 text-right", justify === "between" ? "flex-1" : "flex-none")}>
61
61
  {next ? (
62
- <Button asChild variant="ghost" className={cn("max-w-full justify-end px-0", buttonClassName)}>
62
+ <Button asChild variant="ghost" size="icon" className={buttonClassName}>
63
63
  <Link href={next.href} title={next.label} className={cn("inline-flex min-w-0 items-center", buttonGapClassName)}>
64
64
  {showLabels ? <span className="truncate">{next.label}</span> : null}
65
65
  <ChevronRight className="h-4 w-4 shrink-0" />
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useMemo, useRef, useState, type CSSProperties, type FC, type ReactNode } from "react";
4
4
  import Link from "next/link";
5
+ import { useTranslations } from "next-intl";
5
6
  import { ChevronDown, ChevronRight } from "lucide-react";
6
7
  import {
7
8
  Sidebar,
@@ -35,16 +36,6 @@ const DEFAULT_TOC_WIDTH_PX = 336;
35
36
  const MIN_TOC_WIDTH_PX = 260;
36
37
  const MAX_TOC_WIDTH_PX = 640;
37
38
 
38
- const loadingSkeleton = (
39
- <div className="pt-4 space-y-2">
40
- <Skeleton className="w-auto h-10 mb-2" />
41
- <Skeleton className="w-auto h-10 mb-2" />
42
- <Skeleton className="w-auto h-10 mb-2 ml-8" />
43
- <Skeleton className="w-auto h-10 mb-2 ml-8" />
44
- <div className="px-2 pt-1 text-xs text-muted-foreground">Inhaltsverzeichnis wird geladen...</div>
45
- </div>
46
- );
47
-
48
39
  export const TocTreePanel: FC<TocTreePanelProps> = ({
49
40
  lang,
50
41
  rootNodeId,
@@ -52,13 +43,25 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
52
43
  initialChildrenByParentId = {},
53
44
  placeholder,
54
45
  headerContent,
55
- headerTitle = "Inhaltsverzeichnis",
46
+ headerTitle,
56
47
  loadChildren,
57
48
  buildNodeHref,
58
49
  defaultWidthPx = DEFAULT_TOC_WIDTH_PX,
59
50
  minWidthPx = MIN_TOC_WIDTH_PX,
60
51
  maxWidthPx = MAX_TOC_WIDTH_PX,
61
52
  }) => {
53
+ const t = useTranslations();
54
+ const resolvedHeaderTitle = headerTitle ?? t("tableOfContent");
55
+ const loadingSkeleton = (
56
+ <div className="pt-4 space-y-2">
57
+ <Skeleton className="w-auto h-10 mb-2" />
58
+ <Skeleton className="w-auto h-10 mb-2" />
59
+ <Skeleton className="w-auto h-10 mb-2 ml-8" />
60
+ <Skeleton className="w-auto h-10 mb-2 ml-8" />
61
+ <div className="px-2 pt-1 text-xs text-muted-foreground">{t("loadingTableOfContents")}</div>
62
+ </div>
63
+ );
64
+
62
65
  const [childrenByParentId, setChildrenByParentId] = useState<Record<string, TocNodeSummary[]>>(initialChildrenByParentId);
63
66
  const [loadingParentIds, setLoadingParentIds] = useState<Set<string>>(new Set());
64
67
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set(activePathIds));
@@ -184,7 +187,7 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
184
187
  {node.hasChildren ? (
185
188
  <button
186
189
  type="button"
187
- aria-label={isExpanded ? "Collapse section" : "Expand section"}
190
+ aria-label={isExpanded ? t("collapseSection") : t("expandSection")}
188
191
  className="h-8 w-8 inline-flex items-center justify-center text-muted-foreground hover:text-foreground shrink-0"
189
192
  onClick={() => toggleExpanded(node.id, node.hasChildren)}
190
193
  >
@@ -237,8 +240,8 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
237
240
  <Sidebar side="left">
238
241
  <SidebarHeader className="border-b px-2 py-2">
239
242
  <div className="flex min-w-0 w-full items-center justify-between gap-2">
240
- <div className="min-w-0 overflow-hidden pl-9 text-sm font-medium text-foreground" title={typeof headerTitle === "string" ? headerTitle : undefined}>
241
- <span className="truncate">{headerTitle}</span>
243
+ <div className="min-w-0 overflow-hidden pl-9 text-sm font-medium text-foreground" title={typeof resolvedHeaderTitle === "string" ? resolvedHeaderTitle : undefined}>
244
+ <span className="truncate">{resolvedHeaderTitle}</span>
242
245
  </div>
243
246
  <div className="ml-auto flex shrink-0 items-center">
244
247
  {headerContent}
@@ -263,8 +266,8 @@ export const TocTreePanel: FC<TocTreePanelProps> = ({
263
266
  </Sidebar>
264
267
  <button
265
268
  type="button"
266
- aria-label="Resize table of contents"
267
- title="Resize table of contents"
269
+ aria-label={t("resizeTableOfContents")}
270
+ title={t("resizeTableOfContents")}
268
271
  className="absolute inset-y-0 -right-2 z-20 hidden w-4 cursor-ew-resize lg:block"
269
272
  onMouseDown={(event) => {
270
273
  resizeStartRef.current = { startX: event.clientX, startWidth: tocWidthPx };
@@ -1,19 +0,0 @@
1
- import { FC } from "react";
2
- import { RenderArticle } from "../render-article";
3
-
4
- interface Props {
5
- articleHtml: string;
6
- }
7
-
8
- export const ArticleContent: FC<Props> = async ({ articleHtml }) => {
9
- return (
10
- <div className="relative flex flex-col gap-4 md:flex-row md:items-start">
11
- <div className="relative min-w-0 flex-1 md:pr-8">
12
- <RenderArticle htmlContent={articleHtml} contentLang="" />
13
- </div>
14
-
15
-
16
-
17
- </div>
18
- )
19
- }