@checkstack/notification-frontend 0.2.36 → 0.3.1
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/CHANGELOG.md +295 -0
- package/package.json +9 -8
- package/src/components/CollapsedGroupTimeline.tsx +63 -0
- package/src/components/NotificationBell.tsx +186 -108
- package/src/components/NotificationSubjects.tsx +154 -0
- package/src/components/NotificationSubscriptionsManager.tsx +239 -0
- package/src/components/SubjectKindRegistry.ts +57 -0
- package/src/components/SubscriptionRow.tsx +191 -0
- package/src/components/SubscriptionSubControlsRegistry.ts +45 -0
- package/src/components/collapse.ts +59 -0
- package/src/index.tsx +23 -0
- package/src/pages/NotificationsPage.tsx +158 -89
- package/tsconfig.json +21 -1
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { useState, useCallback } from "react";
|
|
1
|
+
import { useState, useCallback, type ReactNode } from "react";
|
|
2
2
|
import { Link } from "react-router-dom";
|
|
3
3
|
import { Bell, CheckCheck } from "lucide-react";
|
|
4
4
|
import {
|
|
5
5
|
Badge,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
Popover,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
PopoverTrigger,
|
|
9
|
+
Sheet,
|
|
10
|
+
SheetContent,
|
|
11
|
+
SheetTrigger,
|
|
12
|
+
SheetHeader,
|
|
13
|
+
SheetTitle,
|
|
11
14
|
Button,
|
|
12
15
|
stripMarkdown,
|
|
13
16
|
useToast,
|
|
17
|
+
useIsMobile,
|
|
14
18
|
} from "@checkstack/ui";
|
|
15
19
|
import { useApi, usePluginClient } from "@checkstack/frontend-api";
|
|
16
20
|
import { resolveRoute } from "@checkstack/common";
|
|
@@ -18,6 +22,10 @@ import {
|
|
|
18
22
|
NotificationApi,
|
|
19
23
|
notificationRoutes,
|
|
20
24
|
} from "@checkstack/notification-common";
|
|
25
|
+
import { NotificationSubjects } from "./NotificationSubjects";
|
|
26
|
+
import { groupByCollapseKey, type CollapsedNotification } from "./collapse";
|
|
27
|
+
import { CollapsedGroupTimeline } from "./CollapsedGroupTimeline";
|
|
28
|
+
import { ChevronDown, ChevronUp } from "lucide-react";
|
|
21
29
|
import { authApiRef } from "@checkstack/auth-frontend/api";
|
|
22
30
|
|
|
23
31
|
export const NotificationBell = () => {
|
|
@@ -25,8 +33,24 @@ export const NotificationBell = () => {
|
|
|
25
33
|
const { data: session, isPending: isAuthLoading } = authApi.useSession();
|
|
26
34
|
const notificationClient = usePluginClient(NotificationApi);
|
|
27
35
|
const toast = useToast();
|
|
36
|
+
const isMobile = useIsMobile();
|
|
28
37
|
|
|
29
38
|
const [isOpen, setIsOpen] = useState(false);
|
|
39
|
+
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
|
|
40
|
+
() => new Set(),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const toggleExpanded = useCallback((key: string) => {
|
|
44
|
+
setExpandedGroups((prev) => {
|
|
45
|
+
const next = new Set(prev);
|
|
46
|
+
if (next.has(key)) {
|
|
47
|
+
next.delete(key);
|
|
48
|
+
} else {
|
|
49
|
+
next.add(key);
|
|
50
|
+
}
|
|
51
|
+
return next;
|
|
52
|
+
});
|
|
53
|
+
}, []);
|
|
30
54
|
|
|
31
55
|
// Realtime updates arrive via SignalAutoInvalidator on `[["notification"]]`,
|
|
32
56
|
// so both queries stay fresh without per-component signal handlers.
|
|
@@ -62,7 +86,6 @@ export const NotificationBell = () => {
|
|
|
62
86
|
setIsOpen(false);
|
|
63
87
|
}, []);
|
|
64
88
|
|
|
65
|
-
// Hide notification bell for unauthenticated users
|
|
66
89
|
if (isAuthLoading || !session) {
|
|
67
90
|
return;
|
|
68
91
|
}
|
|
@@ -77,119 +100,174 @@ export const NotificationBell = () => {
|
|
|
77
100
|
);
|
|
78
101
|
}
|
|
79
102
|
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
<
|
|
103
|
+
const trigger = (
|
|
104
|
+
<Button variant="ghost" size="icon" className="relative group">
|
|
105
|
+
<Bell className="h-5 w-5 transition-transform group-hover:scale-110" />
|
|
106
|
+
{unreadCount > 0 && (
|
|
107
|
+
<span className="absolute -top-1 -right-1 flex h-5 min-w-[20px] items-center justify-center">
|
|
108
|
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-destructive opacity-75" />
|
|
109
|
+
<Badge
|
|
110
|
+
variant="destructive"
|
|
111
|
+
className="relative h-5 min-w-[20px] flex items-center justify-center p-0 text-xs font-bold"
|
|
112
|
+
>
|
|
113
|
+
{unreadCount > 99 ? "99+" : unreadCount}
|
|
114
|
+
</Badge>
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
</Button>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const headerActions =
|
|
121
|
+
unreadCount > 0 ? (
|
|
122
|
+
<Button
|
|
123
|
+
variant="ghost"
|
|
124
|
+
size="sm"
|
|
125
|
+
className="h-7 text-xs"
|
|
83
126
|
onClick={() => {
|
|
84
|
-
|
|
127
|
+
void handleMarkAllAsRead();
|
|
85
128
|
}}
|
|
86
129
|
>
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
130
|
+
<CheckCheck className="h-3 w-3 mr-1" />
|
|
131
|
+
Mark all read
|
|
132
|
+
</Button>
|
|
133
|
+
) : undefined;
|
|
134
|
+
|
|
135
|
+
const renderItem = (group: CollapsedNotification): ReactNode => {
|
|
136
|
+
const notification = group.representative;
|
|
137
|
+
const isExpanded = expandedGroups.has(group.key);
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
key={group.key}
|
|
141
|
+
className={`flex flex-col items-start gap-1 px-3 py-2 hover:bg-accent transition-colors ${
|
|
142
|
+
notification.importance === "critical"
|
|
143
|
+
? "border-l-2 border-l-destructive"
|
|
144
|
+
: notification.importance === "warning"
|
|
145
|
+
? "border-l-2 border-l-warning"
|
|
146
|
+
: ""
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
<div className="flex items-center gap-2 w-full">
|
|
150
|
+
<div
|
|
151
|
+
className={`font-medium text-sm flex-1 truncate ${
|
|
152
|
+
notification.importance === "critical"
|
|
153
|
+
? "text-destructive"
|
|
154
|
+
: notification.importance === "warning"
|
|
155
|
+
? "text-warning"
|
|
156
|
+
: "text-foreground"
|
|
157
|
+
}`}
|
|
158
|
+
>
|
|
159
|
+
{notification.title}
|
|
160
|
+
</div>
|
|
161
|
+
{group.collapsed && (
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
className="inline-flex items-center gap-0.5 shrink-0"
|
|
165
|
+
onClick={(e: React.MouseEvent) => {
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
toggleExpanded(group.key);
|
|
168
|
+
}}
|
|
169
|
+
aria-label={
|
|
170
|
+
isExpanded
|
|
171
|
+
? "Collapse update history"
|
|
172
|
+
: "Show update history"
|
|
173
|
+
}
|
|
174
|
+
>
|
|
92
175
|
<Badge
|
|
93
|
-
variant="
|
|
94
|
-
className="
|
|
176
|
+
variant="secondary"
|
|
177
|
+
className="text-[10px] h-5 cursor-pointer hover:bg-accent"
|
|
95
178
|
>
|
|
96
|
-
{
|
|
179
|
+
+{group.count - 1} updates
|
|
180
|
+
{isExpanded ? (
|
|
181
|
+
<ChevronUp className="ml-0.5 h-3 w-3" />
|
|
182
|
+
) : (
|
|
183
|
+
<ChevronDown className="ml-0.5 h-3 w-3" />
|
|
184
|
+
)}
|
|
97
185
|
</Badge>
|
|
98
|
-
</
|
|
186
|
+
</button>
|
|
99
187
|
)}
|
|
100
|
-
</
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
className="h-6 text-xs"
|
|
115
|
-
onClick={() => {
|
|
116
|
-
void handleMarkAllAsRead();
|
|
188
|
+
</div>
|
|
189
|
+
<div className="text-xs text-muted-foreground line-clamp-2">
|
|
190
|
+
{stripMarkdown(notification.body)}
|
|
191
|
+
</div>
|
|
192
|
+
{notification.subjects && notification.subjects.length > 0 && (
|
|
193
|
+
<NotificationSubjects subjects={notification.subjects} />
|
|
194
|
+
)}
|
|
195
|
+
{notification.action && (
|
|
196
|
+
<div className="flex gap-2 mt-1">
|
|
197
|
+
<Link
|
|
198
|
+
to={notification.action.url}
|
|
199
|
+
className="text-xs text-primary hover:underline"
|
|
200
|
+
onClick={(e: React.MouseEvent) => {
|
|
201
|
+
e.stopPropagation();
|
|
117
202
|
}}
|
|
118
203
|
>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
204
|
+
{notification.action.label}
|
|
205
|
+
</Link>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
{group.collapsed && isExpanded && (
|
|
209
|
+
<CollapsedGroupTimeline notifications={group.notifications} />
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
124
214
|
|
|
125
|
-
|
|
126
|
-
<div className="max-h-[400px] overflow-y-auto">
|
|
127
|
-
{recentNotifications.length === 0 ? (
|
|
128
|
-
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
129
|
-
No unread notifications
|
|
130
|
-
</div>
|
|
131
|
-
) : (
|
|
132
|
-
<>
|
|
133
|
-
{recentNotifications.map((notification) => (
|
|
134
|
-
<DropdownMenuItem
|
|
135
|
-
key={notification.id}
|
|
136
|
-
className={`flex flex-col items-start gap-1 px-3 py-2 cursor-pointer ${
|
|
137
|
-
notification.importance === "critical"
|
|
138
|
-
? "border-l-2 border-l-destructive"
|
|
139
|
-
: notification.importance === "warning"
|
|
140
|
-
? "border-l-2 border-l-warning"
|
|
141
|
-
: ""
|
|
142
|
-
}`}
|
|
143
|
-
>
|
|
144
|
-
<div
|
|
145
|
-
className={`font-medium text-sm ${
|
|
146
|
-
notification.importance === "critical"
|
|
147
|
-
? "text-destructive"
|
|
148
|
-
: notification.importance === "warning"
|
|
149
|
-
? "text-warning"
|
|
150
|
-
: "text-foreground"
|
|
151
|
-
}`}
|
|
152
|
-
>
|
|
153
|
-
{notification.title}
|
|
154
|
-
</div>
|
|
155
|
-
<div className="text-xs text-muted-foreground line-clamp-2">
|
|
156
|
-
{stripMarkdown(notification.body)}
|
|
157
|
-
</div>
|
|
158
|
-
{notification.action && (
|
|
159
|
-
<div className="flex gap-2 mt-1">
|
|
160
|
-
<Link
|
|
161
|
-
to={notification.action.url}
|
|
162
|
-
className="text-xs text-primary hover:underline"
|
|
163
|
-
onClick={(e: React.MouseEvent) => {
|
|
164
|
-
e.stopPropagation();
|
|
165
|
-
}}
|
|
166
|
-
>
|
|
167
|
-
{notification.action.label}
|
|
168
|
-
</Link>
|
|
169
|
-
</div>
|
|
170
|
-
)}
|
|
171
|
-
</DropdownMenuItem>
|
|
172
|
-
))}
|
|
173
|
-
</>
|
|
174
|
-
)}
|
|
175
|
-
</div>
|
|
215
|
+
const collapsedGroups = groupByCollapseKey(recentNotifications);
|
|
176
216
|
|
|
177
|
-
|
|
217
|
+
const list =
|
|
218
|
+
collapsedGroups.length === 0 ? (
|
|
219
|
+
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
220
|
+
No unread notifications
|
|
221
|
+
</div>
|
|
222
|
+
) : (
|
|
223
|
+
<>{collapsedGroups.map((g) => renderItem(g))}</>
|
|
224
|
+
);
|
|
178
225
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
226
|
+
const footer = (
|
|
227
|
+
<Link
|
|
228
|
+
to={resolveRoute(notificationRoutes.routes.home)}
|
|
229
|
+
className="block w-full text-center text-sm text-primary hover:bg-accent transition-colors px-3 py-2"
|
|
230
|
+
onClick={handleClose}
|
|
231
|
+
>
|
|
232
|
+
View all notifications
|
|
233
|
+
</Link>
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (isMobile) {
|
|
237
|
+
return (
|
|
238
|
+
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
|
239
|
+
<SheetTrigger asChild>{trigger}</SheetTrigger>
|
|
240
|
+
<SheetContent
|
|
241
|
+
size="full"
|
|
242
|
+
className="flex flex-col p-0"
|
|
243
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
184
244
|
>
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
className="
|
|
188
|
-
>
|
|
189
|
-
|
|
190
|
-
</
|
|
191
|
-
</
|
|
192
|
-
</
|
|
193
|
-
|
|
245
|
+
<SheetHeader className="flex flex-row items-center justify-between gap-2 px-4 py-3 border-b border-border space-y-0">
|
|
246
|
+
<SheetTitle className="text-base">Notifications</SheetTitle>
|
|
247
|
+
<div className="pr-8">{headerActions}</div>
|
|
248
|
+
</SheetHeader>
|
|
249
|
+
<div className="flex-1 overflow-y-auto">{list}</div>
|
|
250
|
+
<div className="border-t border-border">{footer}</div>
|
|
251
|
+
</SheetContent>
|
|
252
|
+
</Sheet>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
258
|
+
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
259
|
+
<PopoverContent
|
|
260
|
+
className="w-80 p-0"
|
|
261
|
+
align="end"
|
|
262
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
263
|
+
>
|
|
264
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-border">
|
|
265
|
+
<span className="font-semibold text-sm">Notifications</span>
|
|
266
|
+
{headerActions}
|
|
267
|
+
</div>
|
|
268
|
+
<div className="max-h-[400px] overflow-y-auto">{list}</div>
|
|
269
|
+
<div className="border-t border-border">{footer}</div>
|
|
270
|
+
</PopoverContent>
|
|
271
|
+
</Popover>
|
|
194
272
|
);
|
|
195
273
|
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
Popover,
|
|
5
|
+
PopoverContent,
|
|
6
|
+
PopoverTrigger,
|
|
7
|
+
Button,
|
|
8
|
+
usePerformance,
|
|
9
|
+
} from "@checkstack/ui";
|
|
10
|
+
import type { NotificationSubject } from "@checkstack/notification-common";
|
|
11
|
+
import { getSubjectKindRenderer } from "./SubjectKindRegistry";
|
|
12
|
+
|
|
13
|
+
const STATUS_DOT_COLOR: Record<
|
|
14
|
+
NonNullable<NotificationSubject["status"]>,
|
|
15
|
+
string
|
|
16
|
+
> = {
|
|
17
|
+
healthy: "bg-emerald-500",
|
|
18
|
+
degraded: "bg-amber-500",
|
|
19
|
+
unhealthy: "bg-rose-500",
|
|
20
|
+
unknown: "bg-zinc-400",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface NotificationSubjectsProps {
|
|
24
|
+
subjects: NotificationSubject[];
|
|
25
|
+
/** Inline cap before collapsing the rest into a "+N" overflow popover. */
|
|
26
|
+
maxVisible?: number;
|
|
27
|
+
/** Optional callback fired when a chip's link is clicked. */
|
|
28
|
+
onSubjectClick?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compact chip list of affected entities. Each chip shows the kind icon (via
|
|
33
|
+
* the kind registry) and the subject name; clicking links to the subject's
|
|
34
|
+
* url when present. Status maps to a small colored dot.
|
|
35
|
+
*
|
|
36
|
+
* On low-power devices, transitions are disabled.
|
|
37
|
+
*/
|
|
38
|
+
export function NotificationSubjects({
|
|
39
|
+
subjects,
|
|
40
|
+
maxVisible = 3,
|
|
41
|
+
onSubjectClick,
|
|
42
|
+
}: NotificationSubjectsProps) {
|
|
43
|
+
const [overflowOpen, setOverflowOpen] = useState(false);
|
|
44
|
+
const { isLowPower } = usePerformance();
|
|
45
|
+
|
|
46
|
+
if (subjects.length === 0) return <></>;
|
|
47
|
+
|
|
48
|
+
const visible = subjects.slice(0, maxVisible);
|
|
49
|
+
const overflow = subjects.slice(maxVisible);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="flex flex-wrap items-center gap-1.5 mt-2">
|
|
53
|
+
{visible.map((subject) => (
|
|
54
|
+
<SubjectChip
|
|
55
|
+
key={`${subject.kind}:${subject.id}`}
|
|
56
|
+
subject={subject}
|
|
57
|
+
isLowPower={isLowPower}
|
|
58
|
+
onClick={onSubjectClick}
|
|
59
|
+
/>
|
|
60
|
+
))}
|
|
61
|
+
{overflow.length > 0 && (
|
|
62
|
+
<Popover open={overflowOpen} onOpenChange={setOverflowOpen}>
|
|
63
|
+
<PopoverTrigger asChild>
|
|
64
|
+
<Button
|
|
65
|
+
type="button"
|
|
66
|
+
variant="outline"
|
|
67
|
+
size="sm"
|
|
68
|
+
className="h-6 px-2 text-xs"
|
|
69
|
+
>
|
|
70
|
+
+{overflow.length} more
|
|
71
|
+
</Button>
|
|
72
|
+
</PopoverTrigger>
|
|
73
|
+
<PopoverContent
|
|
74
|
+
className="w-72 p-2"
|
|
75
|
+
align="start"
|
|
76
|
+
onCloseAutoFocus={(e) => {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<div className="flex flex-col gap-1">
|
|
81
|
+
{overflow.map((subject) => (
|
|
82
|
+
<SubjectChip
|
|
83
|
+
key={`${subject.kind}:${subject.id}`}
|
|
84
|
+
subject={subject}
|
|
85
|
+
isLowPower={isLowPower}
|
|
86
|
+
onClick={() => {
|
|
87
|
+
setOverflowOpen(false);
|
|
88
|
+
onSubjectClick?.();
|
|
89
|
+
}}
|
|
90
|
+
fullWidth
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</PopoverContent>
|
|
95
|
+
</Popover>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface SubjectChipProps {
|
|
102
|
+
subject: NotificationSubject;
|
|
103
|
+
isLowPower: boolean;
|
|
104
|
+
onClick?: () => void;
|
|
105
|
+
fullWidth?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function SubjectChip({
|
|
109
|
+
subject,
|
|
110
|
+
isLowPower,
|
|
111
|
+
onClick,
|
|
112
|
+
fullWidth,
|
|
113
|
+
}: SubjectChipProps) {
|
|
114
|
+
const renderer = getSubjectKindRenderer(subject.kind);
|
|
115
|
+
const Icon = renderer.icon;
|
|
116
|
+
const dotColor = subject.status
|
|
117
|
+
? STATUS_DOT_COLOR[subject.status]
|
|
118
|
+
: undefined;
|
|
119
|
+
|
|
120
|
+
const baseClass = [
|
|
121
|
+
"inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md",
|
|
122
|
+
"border border-border bg-card text-foreground",
|
|
123
|
+
"text-xs font-medium max-w-[14rem] truncate",
|
|
124
|
+
isLowPower ? "" : "transition-colors hover:bg-accent",
|
|
125
|
+
fullWidth ? "w-full" : "",
|
|
126
|
+
]
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.join(" ");
|
|
129
|
+
|
|
130
|
+
const content = (
|
|
131
|
+
<>
|
|
132
|
+
<Icon className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
133
|
+
{dotColor && (
|
|
134
|
+
<span
|
|
135
|
+
className={`inline-block h-2 w-2 rounded-full shrink-0 ${dotColor}`}
|
|
136
|
+
aria-label={subject.status ?? undefined}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
<span className="truncate" title={`${renderer.label}: ${subject.name}`}>
|
|
140
|
+
{subject.name}
|
|
141
|
+
</span>
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (subject.url) {
|
|
146
|
+
return (
|
|
147
|
+
<Link to={subject.url} className={baseClass} onClick={onClick}>
|
|
148
|
+
{content}
|
|
149
|
+
</Link>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return <span className={baseClass}>{content}</span>;
|
|
154
|
+
}
|