@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,124 +0,0 @@
1
- "use client";
2
-
3
- import React, { FC, Fragment, ReactNode } from "react";
4
- import {
5
- Breadcrumb as BreadcrumbComponent,
6
- BreadcrumbEllipsis,
7
- BreadcrumbItem,
8
- BreadcrumbLink,
9
- BreadcrumbList,
10
- BreadcrumbPage,
11
- BreadcrumbSeparator
12
- } from "@c-rex/ui/breadcrumb";
13
- import { Button } from "@c-rex/ui/button";
14
- import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@c-rex/ui/drawer";
15
- import { TreeOfContent } from "@c-rex/interfaces";
16
- import { useTranslations } from 'next-intl';
17
- import Link from "next/link"
18
- import { useBreakpoint } from "@c-rex/ui/hooks"
19
- import { DEVICE_OPTIONS } from "@c-rex/constants";
20
- import { generateBreadcrumbItems } from "@c-rex/utils";
21
-
22
- interface BreadcrumbProps {
23
- tree: TreeOfContent[];
24
- lang: string;
25
- }
26
-
27
- export const Breadcrumb: FC<BreadcrumbProps> = ({ tree, lang }) => {
28
- const t = useTranslations("breadcrumbs");
29
- const items = generateBreadcrumbItems(tree);
30
- const device = useBreakpoint();
31
- const isMobile = device !== null && (device === DEVICE_OPTIONS.MOBILE || device === DEVICE_OPTIONS.TABLET);
32
-
33
- if (!items) return null
34
-
35
- const lastItem = items[items.length - 1] as TreeOfContent;
36
-
37
- const renderDrawer = () => {
38
- if (items.length < 2) return null
39
-
40
- return (
41
- <>
42
- <BreadcrumbItem>
43
- <Drawer>
44
- <DrawerTrigger asChild>
45
- <BreadcrumbEllipsis className="h-4 w-4" />
46
- </DrawerTrigger>
47
- <DrawerContent>
48
- <DrawerHeader className="text-left">
49
- <DrawerTitle>Navigate to</DrawerTitle>
50
- <DrawerDescription>
51
- Select a page to navigate to.
52
- </DrawerDescription>
53
- </DrawerHeader>
54
-
55
- <div className="grid gap-1 px-4">
56
- {items.slice(0, -1).map((item) => (
57
- <Link
58
- key={item.linkId}
59
- href={`/topics/${item.linkId}/content`}
60
- className="py-1 text-sm"
61
- >
62
- {item.label}
63
- </Link>
64
- ))}
65
- </div>
66
-
67
- <DrawerFooter className="pt-4">
68
- <DrawerClose asChild>
69
- <Button variant="outline">Close</Button>
70
- </DrawerClose>
71
- </DrawerFooter>
72
- </DrawerContent>
73
- </Drawer>
74
- </BreadcrumbItem>
75
- <BreadcrumbSeparator />
76
- </>
77
-
78
- );
79
- };
80
-
81
- const renderLink = (showLink: boolean, item: TreeOfContent): ReactNode => {
82
- if (showLink) {
83
- return <BreadcrumbLink href={`/topics/${item.linkId}/content`}>{item.label}</BreadcrumbLink>;
84
- }
85
-
86
- return <BreadcrumbPage>{item.label}</BreadcrumbPage>;
87
- };
88
-
89
- return (
90
- <BreadcrumbComponent lang={lang} className="hidden sm:block">
91
- <BreadcrumbList>
92
- <BreadcrumbItem>
93
- <BreadcrumbLink asChild>
94
- <BreadcrumbLink href="/">{t("home")}</BreadcrumbLink>
95
- </BreadcrumbLink>
96
- </BreadcrumbItem>
97
- <BreadcrumbSeparator />
98
-
99
- {isMobile ? renderDrawer() : (
100
- <>
101
- {items.slice(0, -1).map((item, index) => {
102
- const isLast = index === items.length - 1;
103
-
104
- return (
105
- <Fragment key={`${item.label}-fragment`}>
106
- <BreadcrumbItem key={`${item.label}-item`}>
107
- {renderLink(!isLast, item)}
108
- </BreadcrumbItem>
109
- {!isLast && (
110
- <BreadcrumbSeparator key={`${item.label}-separator`} />
111
- )}
112
- </Fragment>
113
- );
114
- })}
115
- </>
116
- )}
117
-
118
- <BreadcrumbPage>
119
- {renderLink(false, lastItem)}
120
- </BreadcrumbPage>
121
- </BreadcrumbList>
122
- </BreadcrumbComponent>
123
- );
124
- };
@@ -1,68 +0,0 @@
1
- import {
2
- SidebarGroup,
3
- SidebarMenu,
4
- SidebarMenuButton,
5
- SidebarMenuItem,
6
- SidebarMenuSub,
7
- SidebarMenuSubButton,
8
- SidebarMenuSubItem,
9
- } from "@c-rex/ui/sidebar";
10
- import { DirectoryNodeModel, TreeOfContent as TreeOfContentModel } from "@c-rex/interfaces";
11
- import { generateTreeOfContent } from "@c-rex/utils/directoryNodes";
12
- import Link from "next/link";
13
- import { Suspense } from "react";
14
- import { Skeleton } from "@c-rex/ui/skeleton";
15
-
16
- interface SidebarProps {
17
- directoryNodes?: DirectoryNodeModel[];
18
- tree?: TreeOfContentModel[];
19
- }
20
-
21
- export async function TreeOfContent({ directoryNodes, tree: providedTree }: SidebarProps) {
22
- const tree = providedTree ?? (directoryNodes ? await generateTreeOfContent(directoryNodes) : []);
23
-
24
- return (
25
- <Suspense
26
- fallback={
27
- <SidebarGroup className="pt-4">
28
- <Skeleton className="w-auto h-10 mb-2" />
29
- <Skeleton className="w-auto h-10 mb-2" />
30
- <Skeleton className="w-auto h-10 mb-2 ml-8" />
31
- <Skeleton className="w-auto h-10 mb-2 ml-8" />
32
- <Skeleton className="w-auto h-10 mb-2 ml-8" />
33
- <Skeleton className="w-auto h-10 mb-2 ml-8" />
34
- <Skeleton className="w-auto h-10 mb-2" />
35
- </SidebarGroup>
36
- }
37
- >
38
- <SidebarMenu>
39
- {tree.map((item) => (
40
- <SidebarMenuItem key={item.id}>
41
- <SidebarMenuButton asChild isActive={item.active}>
42
- <Link prefetch={false} href={`/topics/${item.linkId}/pages`} title={item.label}>
43
- {item.label}
44
- </Link>
45
- </SidebarMenuButton>
46
-
47
- {item.children?.length ? (
48
- <SidebarMenuSub>
49
- {item.children.map((item) => (
50
- <SidebarMenuSubItem key={item.label}>
51
- <SidebarMenuSubButton
52
- asChild
53
- isActive={item.active}
54
- >
55
- <Link prefetch={false} href={`/topics/${item.linkId}/pages`} title={item.label}>
56
- {item.label}
57
- </Link>
58
- </SidebarMenuSubButton>
59
- </SidebarMenuSubItem>
60
- ))}
61
- </SidebarMenuSub>
62
- ) : null}
63
- </SidebarMenuItem>
64
- ))}
65
- </SidebarMenu>
66
- </Suspense>
67
- );
68
- }
@@ -1,75 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo, useRef, useEffect } from "react";
4
- import parse from "html-react-parser";
5
- import { useQueryState } from "nuqs";
6
- import { useHighlight } from "@c-rex/contexts/highlight-provider";
7
- import { useHighlightStore } from "./stores/highlight-store";
8
- import styles from "./render-article.module.css";
9
-
10
- type Props = {
11
- htmlContent: string;
12
- contentLang?: string;
13
- };
14
-
15
- export const RenderArticle = ({ htmlContent, contentLang }: Props) => {
16
- const [query] = useQueryState("q");
17
- const containerRef = useRef<HTMLElement | null>(null);
18
- const enableHighlight = useHighlightStore((state) => state.enable);
19
- const { registerContainer } = useHighlight();
20
-
21
- const highlightedContent = useMemo(() => {
22
- if (!enableHighlight) return parse(htmlContent);
23
- if (!query) return parse(htmlContent);
24
-
25
- const terms = query
26
- .split(/[+ ]/)
27
- .map((t) => t.trim())
28
- .filter(Boolean);
29
-
30
- if (terms.length === 0) return parse(htmlContent);
31
-
32
- const escaped = terms.map((t) =>
33
- t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
34
- );
35
- const regex = new RegExp(`(${escaped.join("|")})`, "gi");
36
-
37
- return parse(htmlContent, {
38
- replace: (domNode: any) => {
39
- if (domNode.type === "text") {
40
- const parts = domNode.data.split(regex);
41
- if (parts.length === 1) return;
42
- return (
43
- <>
44
- {parts.map((part: any, i: any) =>
45
- regex.test(part) ? (
46
- <mark key={i} className="bg-yellow-200">
47
- {part}
48
- </mark>
49
- ) : (
50
- part
51
- )
52
- )}
53
- </>
54
- );
55
- }
56
- },
57
- });
58
- }, [htmlContent, query, enableHighlight]);
59
-
60
- useEffect(() => {
61
- registerContainer(containerRef.current);
62
- }, [highlightedContent, registerContainer]);
63
-
64
- return (
65
- <main
66
- ref={containerRef}
67
- id="ids-content"
68
- data-content-scope="dita"
69
- lang={contentLang}
70
- className={`ids-content ids-content--dita-ot ${styles.idsContent}`}
71
- >
72
- {highlightedContent}
73
- </main>
74
- );
75
- };
@@ -1,4 +0,0 @@
1
- export {
2
- TaxonomyRestrictionMenu as RestrictionMenuContainer,
3
- type TaxonomyRestrictionMenuProps as RestrictionMenuContainerProps,
4
- } from "./taxonomy-restriction-menu";
@@ -1,4 +0,0 @@
1
- export {
2
- RestrictionSelectionMenu as RestrictionMenu,
3
- type RestrictionSelectionMenuProps as RestrictionMenuProps,
4
- } from "./restriction-selection-menu";
@@ -1,54 +0,0 @@
1
- import { useFavoritesStore } from "../favorites-store";
2
-
3
- describe("favorites-store", () => {
4
- beforeEach(() => {
5
- useFavoritesStore.setState({
6
- favorites: [],
7
- documents: {},
8
- });
9
- });
10
-
11
- it("adds a document and its first topic without duplicating favorites", () => {
12
- const { favoriteDocument, favoriteTopic } = useFavoritesStore.getState();
13
-
14
- favoriteDocument("DOC-1", "Document 1");
15
- favoriteTopic("DOC-1", "TOPIC-1", "Topic 1", "red");
16
-
17
- const state = useFavoritesStore.getState();
18
-
19
- expect(state.favorites).toHaveLength(2);
20
- expect(state.favorites.map((item) => item.id).sort()).toEqual(["DOC-1", "TOPIC-1"]);
21
- expect(state.documents["DOC-1"]).toEqual({
22
- label: "Document 1",
23
- topics: [{ id: "TOPIC-1", label: "Topic 1", color: "red" }],
24
- });
25
- });
26
-
27
- it("does not duplicate a document or topic when favorited multiple times", () => {
28
- const { favoriteDocument, favoriteTopic } = useFavoritesStore.getState();
29
-
30
- favoriteDocument("DOC-1", "Document 1");
31
- favoriteDocument("DOC-1", "Document 1");
32
- favoriteTopic("DOC-1", "TOPIC-1", "Topic 1", "red");
33
- favoriteTopic("DOC-1", "TOPIC-1", "Topic 1", "red");
34
-
35
- const state = useFavoritesStore.getState();
36
-
37
- expect(state.favorites).toHaveLength(2);
38
- expect(state.documents["DOC-1"]?.topics).toHaveLength(1);
39
- });
40
-
41
- it("does not duplicate the document marker when a topic is favorited after the document", () => {
42
- const { favoriteDocument, favoriteTopic } = useFavoritesStore.getState();
43
-
44
- favoriteDocument("DOC-1", "Document 1");
45
- favoriteTopic("DOC-1", "TOPIC-1", "Topic 1", "red");
46
- favoriteTopic("DOC-1", "TOPIC-2", "Topic 2", "blue");
47
-
48
- const state = useFavoritesStore.getState();
49
- const documentEntries = state.favorites.filter((item) => item.id === "DOC-1");
50
-
51
- expect(documentEntries).toHaveLength(1);
52
- expect(state.documents["DOC-1"]?.topics).toHaveLength(2);
53
- });
54
- });
@@ -1,163 +0,0 @@
1
- import { Favorite } from "@c-rex/types";
2
- import { create } from "zustand";
3
- import { persist } from "zustand/middleware";
4
-
5
- type FavoriteDocumentState = { topics: Favorite[], label?: string };
6
- type FavoriteDocumentsMap = Record<string, FavoriteDocumentState>;
7
-
8
- type FavoritesStore = {
9
- favorites: Favorite[];
10
- documents: FavoriteDocumentsMap;
11
- favoriteTopic: (documentId: string, id: string, label: string, color: string) => void;
12
- unfavoriteTopic: (documentId: string, id: string) => void;
13
- favoriteDocument: (id: string, label: string) => void;
14
- unfavoriteDocument: (id: string) => void;
15
- };
16
-
17
- export const useFavoritesStore = create<FavoritesStore>()(
18
- persist((set) => ({
19
- documents: {},
20
- favorites: [],
21
- favoriteTopic: (documentId: string, id: string, label: string, color: string) =>
22
- set((state) => {
23
- const documents = favoriteTopic(state.documents, documentId, id, label, color);
24
- const favorites = upsertFavorites(state.favorites, [
25
- { id, label, color },
26
- { id: documentId, label: state.documents[documentId]?.label || "", color: "" },
27
- ]);
28
-
29
- if (documents === state.documents && favorites === state.favorites) {
30
- return state;
31
- }
32
-
33
- return { documents, favorites };
34
- }),
35
- unfavoriteTopic: (documentId: string, id: string) =>
36
- set((state) => ({
37
- documents: unfavoriteTopic(state.documents, documentId, id),
38
- favorites: state.favorites.filter((topic) => topic.id !== id),
39
- })),
40
- favoriteDocument: (id: string, label: string) =>
41
- set((state) => {
42
- const documents = upsertDocument(state.documents, id, label);
43
- const favorites = upsertFavorites(state.favorites, [{ id, label, color: "" }]);
44
-
45
- if (documents === state.documents && favorites === state.favorites) {
46
- return state;
47
- }
48
-
49
- return {
50
- documents,
51
- favorites,
52
- };
53
- }),
54
- unfavoriteDocument: (id: string) =>
55
- set((state) => {
56
- const documentsCopy = { ...state.documents };
57
- if (!documentsCopy[id]) {
58
- return state;
59
- }
60
-
61
- const favoritesToRemove = documentsCopy[id]?.topics.map(topic => topic.id) || [];
62
- const newFavorites = state.favorites.filter(fav => fav.id !== id && !favoritesToRemove.includes(fav.id));
63
- delete documentsCopy[id];
64
- return { documents: documentsCopy, favorites: newFavorites };
65
- }),
66
- }), {
67
- name: "c-rex-favorites",
68
- })
69
- );
70
-
71
-
72
- const favoriteTopic = (documents: FavoriteDocumentsMap, documentId: string, id: string, label: string, color: string): FavoriteDocumentsMap => {
73
- const currentDocument = documents[documentId];
74
- const currentTopics = currentDocument?.topics ?? [];
75
- if (currentTopics.some((topic) => topic.id === id)) {
76
- return documents;
77
- }
78
-
79
- return {
80
- ...documents,
81
- [documentId]: {
82
- ...currentDocument,
83
- topics: [...currentTopics, { id, label, color }],
84
- },
85
- };
86
- };
87
-
88
- const unfavoriteTopic = (documents: FavoriteDocumentsMap, documentId: string, id: string): FavoriteDocumentsMap => {
89
- const currentDocument = documents[documentId];
90
- if (!currentDocument) {
91
- return documents;
92
- }
93
-
94
- return {
95
- ...documents,
96
- [documentId]: {
97
- ...currentDocument,
98
- topics: currentDocument.topics.filter(topic => topic.id !== id),
99
- },
100
- };
101
- };
102
-
103
- const upsertDocument = (documents: FavoriteDocumentsMap, documentId: string, label: string): FavoriteDocumentsMap => {
104
- const currentDocument = documents[documentId];
105
-
106
- if (!currentDocument) {
107
- return {
108
- ...documents,
109
- [documentId]: { topics: [], label },
110
- };
111
- }
112
-
113
- if (currentDocument.label === label) {
114
- return documents;
115
- }
116
-
117
- return {
118
- ...documents,
119
- [documentId]: {
120
- ...currentDocument,
121
- label,
122
- },
123
- };
124
- };
125
-
126
- const upsertFavorites = (favorites: Favorite[], entries: Favorite[]): Favorite[] => {
127
- let nextFavorites = favorites;
128
-
129
- for (const entry of entries) {
130
- nextFavorites = upsertFavorite(nextFavorites, entry);
131
- }
132
-
133
- return nextFavorites;
134
- };
135
-
136
- const upsertFavorite = (favorites: Favorite[], entry: Favorite): Favorite[] => {
137
- const index = favorites.findIndex((favorite) => favorite.id === entry.id);
138
- if (index === -1) {
139
- return [...favorites, entry];
140
- }
141
-
142
- const current = favorites[index];
143
- if (!current) {
144
- return [...favorites, entry];
145
- }
146
-
147
- const next = {
148
- ...current,
149
- label: current.label || entry.label,
150
- color: current.color || entry.color,
151
- };
152
-
153
- if (
154
- current.label === next.label &&
155
- current.color === next.color
156
- ) {
157
- return favorites;
158
- }
159
-
160
- const copy = [...favorites];
161
- copy[index] = next;
162
- return copy;
163
- };