@messagevisor/catalog 0.0.1 → 0.1.0
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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/assets/index-CfGbXx4X.css +1 -0
- package/dist/assets/index-r8ugP5JL.js +73 -0
- package/dist/favicon.png +0 -0
- package/dist/index.html +14 -0
- package/dist/logo-text.png +0 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/node/formatExamplePreview.d.ts +10 -0
- package/lib/node/formatExamplePreview.js +79 -0
- package/lib/node/formatExamplePreview.js.map +1 -0
- package/lib/node/index.d.ts +191 -0
- package/lib/node/index.js +1645 -0
- package/lib/node/index.js.map +1 -0
- package/package.json +59 -13
- package/src/App.tsx +73 -0
- package/src/api.spec.ts +42 -0
- package/src/api.ts +87 -0
- package/src/catalogBrandAssets.ts +8 -0
- package/src/components/details/ConditionTree.tsx +146 -0
- package/src/components/details/FieldGrid.tsx +16 -0
- package/src/components/details/GroupSegmentTree.tsx +73 -0
- package/src/components/details/MarkdownContent.tsx +23 -0
- package/src/components/details/TranslationsTable.tsx +263 -0
- package/src/components/details/UsageLinks.tsx +29 -0
- package/src/components/history/HistoryTimeline.tsx +122 -0
- package/src/components/layout/AppShell.tsx +338 -0
- package/src/components/layout/PageHeader.tsx +13 -0
- package/src/components/layout/Tabs.tsx +35 -0
- package/src/components/lists/EntityList.tsx +451 -0
- package/src/components/ui/Badge.tsx +21 -0
- package/src/components/ui/Button.tsx +12 -0
- package/src/components/ui/Card.tsx +9 -0
- package/src/components/ui/CodeBlock.tsx +7 -0
- package/src/components/ui/EmptyState.tsx +8 -0
- package/src/components/ui/Input.tsx +12 -0
- package/src/components/ui/LabelValueBadge.tsx +55 -0
- package/src/config.ts +2 -0
- package/src/context/CatalogContext.tsx +50 -0
- package/src/entityTypes.ts +49 -0
- package/src/index.ts +1 -0
- package/src/main.tsx +28 -0
- package/src/node/formatExamplePreview.ts +85 -0
- package/src/node/index.spec.ts +713 -0
- package/src/node/index.ts +2007 -0
- package/src/pages/EntityDetailPage.tsx +3345 -0
- package/src/pages/HistoryPage.tsx +26 -0
- package/src/pages/HomePage.tsx +21 -0
- package/src/pages/ListPage.tsx +59 -0
- package/src/styles.css +95 -0
- package/src/theme.ts +36 -0
- package/src/types.ts +127 -0
- package/src/utils/formatCatalogTimestamp.ts +77 -0
- package/src/utils/hashTranslationValue.spec.ts +20 -0
- package/src/utils/hashTranslationValue.ts +22 -0
- package/src/utils/searchQuery.ts +46 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { NavLink, useLocation, useNavigate } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
import { fetchIndex } from "../../api";
|
|
6
|
+
import {
|
|
7
|
+
encodeRouteSegment,
|
|
8
|
+
entityPaths,
|
|
9
|
+
entityPathToType,
|
|
10
|
+
entityLabels,
|
|
11
|
+
getBasePath,
|
|
12
|
+
} from "../../entityTypes";
|
|
13
|
+
import { CATALOG_NAV_LOGO_MARK_SRC, CATALOG_NAV_LOGO_WORDMARK_SRC } from "../../catalogBrandAssets";
|
|
14
|
+
import { themeClasses } from "../../theme";
|
|
15
|
+
import { useCatalog } from "../../context/CatalogContext";
|
|
16
|
+
import type { CatalogIndex, EntityPath } from "../../types";
|
|
17
|
+
|
|
18
|
+
function sidebarClass({ isActive }: { isActive: boolean }) {
|
|
19
|
+
return [
|
|
20
|
+
"flex items-center justify-between rounded-lg px-3 py-2 text-sm font-bold",
|
|
21
|
+
isActive ? "bg-header-active text-header-text" : "text-muted hover:bg-elevated hover:text-text",
|
|
22
|
+
].join(" ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function Sidebar(props: { setKey?: string }) {
|
|
26
|
+
const [index, setIndex] = React.useState<CatalogIndex | null>(null);
|
|
27
|
+
const basePath = getBasePath(props.setKey);
|
|
28
|
+
const historyLabel = "History";
|
|
29
|
+
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
setIndex(null);
|
|
32
|
+
fetchIndex(props.setKey)
|
|
33
|
+
.then(setIndex)
|
|
34
|
+
.catch(() => setIndex(null));
|
|
35
|
+
}, [props.setKey]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<aside className="rounded-lg bg-surface p-4 shadow-md ring-1 ring-black/5 md:w-56">
|
|
39
|
+
<div className="mb-3 text-xs font-black uppercase tracking-wide text-muted">
|
|
40
|
+
<span className="block px-3 text-muted">{props.setKey ? "Set" : "Project"}</span>
|
|
41
|
+
</div>
|
|
42
|
+
<nav className="space-y-1">
|
|
43
|
+
{entityPaths.map((entityPath) => {
|
|
44
|
+
const type = entityPathToType[entityPath];
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<NavLink key={entityPath} to={`${basePath}/${entityPath}`} className={sidebarClass}>
|
|
48
|
+
<span>{entityLabels[type].plural}</span>
|
|
49
|
+
<span className="rounded-full bg-pill px-2 py-0.5 text-xs font-black text-header">
|
|
50
|
+
{index?.counts[type] ?? "-"}
|
|
51
|
+
</span>
|
|
52
|
+
</NavLink>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
<NavLink
|
|
56
|
+
to={`${basePath}/history`}
|
|
57
|
+
className={({ isActive }) =>
|
|
58
|
+
[
|
|
59
|
+
"mt-4 block rounded-lg px-3 py-2 text-sm font-bold",
|
|
60
|
+
isActive
|
|
61
|
+
? "bg-header-active text-header-text"
|
|
62
|
+
: "text-muted hover:bg-elevated hover:text-text",
|
|
63
|
+
].join(" ")
|
|
64
|
+
}
|
|
65
|
+
>
|
|
66
|
+
{historyLabel}
|
|
67
|
+
</NavLink>
|
|
68
|
+
</nav>
|
|
69
|
+
</aside>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isEntityPath(value: string): value is EntityPath {
|
|
74
|
+
return entityPaths.indexOf(value as EntityPath) !== -1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasEntity(index: CatalogIndex, entityPath: EntityPath, entityKey: string) {
|
|
78
|
+
const type = entityPathToType[entityPath];
|
|
79
|
+
const entities = index.entities[type] || [];
|
|
80
|
+
|
|
81
|
+
return entities.some((entity) => entity.key === entityKey);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function getSetSwitchPath(pathname: string, nextSetKey: string) {
|
|
85
|
+
const encodedSetKey = encodeRouteSegment(nextSetKey);
|
|
86
|
+
const listMatch = pathname.match(/^\/sets\/[^/]+\/([^/]+)$/);
|
|
87
|
+
|
|
88
|
+
if (listMatch && isEntityPath(listMatch[1])) {
|
|
89
|
+
return `/sets/${encodedSetKey}/${listMatch[1]}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (pathname.match(/^\/sets\/[^/]+\/history$/)) {
|
|
93
|
+
return `/sets/${encodedSetKey}/history`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const detailMatch = pathname.match(/^\/sets\/[^/]+\/([^/]+)\/([^/]+)(\/.*)?$/);
|
|
97
|
+
|
|
98
|
+
if (detailMatch && isEntityPath(detailMatch[1])) {
|
|
99
|
+
const entityPath = detailMatch[1];
|
|
100
|
+
const entityKey = decodeURIComponent(detailMatch[2]);
|
|
101
|
+
const suffix = detailMatch[3] || "";
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const index = await fetchIndex(nextSetKey);
|
|
105
|
+
|
|
106
|
+
if (hasEntity(index, entityPath, entityKey)) {
|
|
107
|
+
return `/sets/${encodedSetKey}/${entityPath}/${encodeRouteSegment(entityKey)}${suffix}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `/sets/${encodedSetKey}/${entityPath}`;
|
|
111
|
+
} catch {
|
|
112
|
+
return `/sets/${encodedSetKey}/${entityPath}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return `/sets/${encodedSetKey}/messages`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function SetSwitcher(props: { currentSetKey?: string }) {
|
|
120
|
+
const { manifest } = useCatalog();
|
|
121
|
+
const location = useLocation();
|
|
122
|
+
const navigate = useNavigate();
|
|
123
|
+
const setSelectId = React.useId();
|
|
124
|
+
const setSelectRef = React.useRef<HTMLSelectElement | null>(null);
|
|
125
|
+
const selectedSetKey = props.currentSetKey || manifest.setKeys[0] || "";
|
|
126
|
+
|
|
127
|
+
if (!manifest.sets || manifest.setKeys.length === 0) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function openSetPicker() {
|
|
132
|
+
const select = setSelectRef.current;
|
|
133
|
+
|
|
134
|
+
if (!select) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
select.focus();
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
typeof (select as HTMLSelectElement & { showPicker?: () => void }).showPicker === "function"
|
|
142
|
+
) {
|
|
143
|
+
(select as HTMLSelectElement & { showPicker: () => void }).showPicker();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<label
|
|
149
|
+
htmlFor={setSelectId}
|
|
150
|
+
className="relative inline-flex cursor-pointer items-center gap-2 rounded-lg bg-header-active px-3 py-1.5 text-sm font-semibold text-header-text"
|
|
151
|
+
onClick={(event) => {
|
|
152
|
+
if (event.target instanceof HTMLSelectElement) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
openSetPicker();
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<span className="text-xs font-black uppercase tracking-wide text-pill">Set</span>
|
|
160
|
+
<select
|
|
161
|
+
id={setSelectId}
|
|
162
|
+
ref={setSelectRef}
|
|
163
|
+
value={selectedSetKey}
|
|
164
|
+
onChange={async (event) => {
|
|
165
|
+
const nextSetKey = event.target.value;
|
|
166
|
+
|
|
167
|
+
navigate(await getSetSwitchPath(location.pathname, nextSetKey));
|
|
168
|
+
}}
|
|
169
|
+
className="max-w-44 appearance-none bg-transparent pr-7 font-black text-header-text outline-none"
|
|
170
|
+
aria-label="Switch catalog set"
|
|
171
|
+
>
|
|
172
|
+
{manifest.setKeys.map((setKey) => (
|
|
173
|
+
<option key={setKey} value={setKey}>
|
|
174
|
+
{setKey}
|
|
175
|
+
</option>
|
|
176
|
+
))}
|
|
177
|
+
</select>
|
|
178
|
+
<svg
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
viewBox="0 0 20 20"
|
|
181
|
+
fill="none"
|
|
182
|
+
className="pointer-events-none absolute right-3 h-4 w-4 text-pill"
|
|
183
|
+
>
|
|
184
|
+
<path
|
|
185
|
+
d="M6 8l4 4 4-4"
|
|
186
|
+
stroke="currentColor"
|
|
187
|
+
strokeWidth="2"
|
|
188
|
+
strokeLinecap="round"
|
|
189
|
+
strokeLinejoin="round"
|
|
190
|
+
/>
|
|
191
|
+
</svg>
|
|
192
|
+
</label>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function RepositoryIcon(props: { provider?: string }) {
|
|
197
|
+
if (props.provider === "github") {
|
|
198
|
+
return (
|
|
199
|
+
<svg aria-hidden="true" className="h-5 w-5" viewBox="0 0 16 16" fill="currentColor">
|
|
200
|
+
<path d="M8 0C3.58 0 0 3.69 0 8.24c0 3.64 2.29 6.72 5.47 7.81.4.08.55-.18.55-.4 0-.2-.01-.86-.01-1.56-2.01.38-2.53-.5-2.69-.96-.09-.24-.48-.96-.82-1.15-.28-.16-.68-.55-.01-.56.63-.01 1.08.6 1.23.85.72 1.25 1.87.9 2.33.69.07-.54.28-.9.51-1.11-1.78-.21-3.64-.92-3.64-4.07 0-.9.31-1.64.82-2.22-.08-.21-.36-1.05.08-2.19 0 0 .67-.22 2.2.85A7.43 7.43 0 0 1 8 3.94c.68 0 1.36.09 2 .28 1.52-1.07 2.19-.85 2.19-.85.44 1.14.16 1.98.08 2.19.51.58.82 1.32.82 2.22 0 3.16-1.87 3.86-3.65 4.07.29.26.54.76.54 1.54 0 1.11-.01 2.01-.01 2.28 0 .22.15.48.55.4A8.13 8.13 0 0 0 16 8.24C16 3.69 12.42 0 8 0Z" />
|
|
201
|
+
</svg>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (props.provider === "gitlab") {
|
|
206
|
+
return (
|
|
207
|
+
<svg aria-hidden="true" className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
|
208
|
+
<path d="m22.75 9.77-.03-.08-2.17-6.69a.57.57 0 0 0-.55-.39.58.58 0 0 0-.52.35l-1.47 4.48H5.99L4.52 2.96A.58.58 0 0 0 4 2.61a.57.57 0 0 0-.55.39L1.28 9.69l-.03.08a1.54 1.54 0 0 0 .51 1.73l.01.01 10.22 7.43 10.24-7.44.01-.01a1.54 1.54 0 0 0 .51-1.72Z" />
|
|
209
|
+
</svg>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (props.provider === "bitbucket") {
|
|
214
|
+
return (
|
|
215
|
+
<svg aria-hidden="true" className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
|
216
|
+
<path d="M2.19 3.25a.77.77 0 0 0-.76.89l2.7 16.42a1.02 1.02 0 0 0 1 .86H18.9a1.02 1.02 0 0 0 1-.82l2.69-16.46a.77.77 0 0 0-.76-.89H2.19Zm13.36 10.71H9.46l-1.1-5.83h8.25l-1.06 5.83Z" />
|
|
217
|
+
</svg>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isKnownRepositoryProvider(provider?: string) {
|
|
225
|
+
return provider === "github" || provider === "gitlab" || provider === "bitbucket";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function formatGeneratedAt(value: string) {
|
|
229
|
+
const date = new Date(value);
|
|
230
|
+
|
|
231
|
+
if (Number.isNaN(date.getTime())) {
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const localTimestamp = Date.UTC(
|
|
236
|
+
date.getFullYear(),
|
|
237
|
+
date.getMonth(),
|
|
238
|
+
date.getDate(),
|
|
239
|
+
date.getHours(),
|
|
240
|
+
date.getMinutes(),
|
|
241
|
+
date.getSeconds(),
|
|
242
|
+
date.getMilliseconds(),
|
|
243
|
+
);
|
|
244
|
+
const offset = Math.round((localTimestamp - date.getTime()) / 60000);
|
|
245
|
+
const sign = offset >= 0 ? "+" : "-";
|
|
246
|
+
const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, "0");
|
|
247
|
+
const minutes = String(Math.abs(offset) % 60).padStart(2, "0");
|
|
248
|
+
const local = new Date(date.getTime() + offset * 60 * 1000)
|
|
249
|
+
.toISOString()
|
|
250
|
+
.replace("T", " ")
|
|
251
|
+
.replace(/\.\d{3}Z$/, "");
|
|
252
|
+
|
|
253
|
+
return `${local} ${sign}${hours}:${minutes}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function AppShell(props: { children: ReactNode }) {
|
|
257
|
+
const { manifest } = useCatalog();
|
|
258
|
+
const location = useLocation();
|
|
259
|
+
const setKeyMatch = location.pathname.match(/^\/sets\/([^/]+)/);
|
|
260
|
+
const setKey = setKeyMatch ? decodeURIComponent(setKeyMatch[1]) : undefined;
|
|
261
|
+
const showSidebar = !manifest.sets || Boolean(setKey);
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<div className={themeClasses.page}>
|
|
265
|
+
<header className="bg-header">
|
|
266
|
+
<nav className="mx-auto flex max-w-5xl items-center justify-between gap-4 px-3 py-3 sm:px-4">
|
|
267
|
+
<div className="flex min-w-0 flex-1 items-center">
|
|
268
|
+
<NavLink
|
|
269
|
+
to="/"
|
|
270
|
+
className={[
|
|
271
|
+
"flex min-w-0 max-w-full items-center gap-2.5 rounded-lg py-1 pr-2 outline-none",
|
|
272
|
+
"ring-offset-2 ring-offset-header focus-visible:ring-2 focus-visible:ring-header-text",
|
|
273
|
+
].join(" ")}
|
|
274
|
+
aria-label="Messagevisor Catalog home"
|
|
275
|
+
>
|
|
276
|
+
<img
|
|
277
|
+
src={CATALOG_NAV_LOGO_MARK_SRC}
|
|
278
|
+
alt=""
|
|
279
|
+
width={32}
|
|
280
|
+
height={32}
|
|
281
|
+
className="h-8 w-8 shrink-0 object-contain"
|
|
282
|
+
decoding="async"
|
|
283
|
+
/>
|
|
284
|
+
<img
|
|
285
|
+
src={CATALOG_NAV_LOGO_WORDMARK_SRC}
|
|
286
|
+
alt=""
|
|
287
|
+
className="h-auto max-h-4 min-w-0 shrink object-contain object-left pl-2"
|
|
288
|
+
decoding="async"
|
|
289
|
+
/>
|
|
290
|
+
</NavLink>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div className="flex items-center gap-3">
|
|
294
|
+
<SetSwitcher currentSetKey={setKey} />
|
|
295
|
+
{manifest.links?.repository && isKnownRepositoryProvider(manifest.links.provider) && (
|
|
296
|
+
<a
|
|
297
|
+
href={manifest.links.repository}
|
|
298
|
+
target="_blank"
|
|
299
|
+
rel="noreferrer"
|
|
300
|
+
className="rounded-lg px-3 py-2 text-header-text hover:bg-header-active"
|
|
301
|
+
aria-label={`Open ${manifest.links.provider || "repository"}`}
|
|
302
|
+
>
|
|
303
|
+
<RepositoryIcon provider={manifest.links.provider} />
|
|
304
|
+
</a>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
</nav>
|
|
308
|
+
</header>
|
|
309
|
+
|
|
310
|
+
<main className={themeClasses.pageShell}>
|
|
311
|
+
<div className={showSidebar ? "items-start gap-6 md:flex" : ""}>
|
|
312
|
+
{showSidebar && <Sidebar setKey={setKey} />}
|
|
313
|
+
<div className={["min-w-0", showSidebar ? "flex-1" : "w-full"].filter(Boolean).join(" ")}>
|
|
314
|
+
<section className="overflow-hidden rounded-lg bg-surface shadow">
|
|
315
|
+
{props.children}
|
|
316
|
+
</section>
|
|
317
|
+
<footer className="mt-4 pt-3 text-center">
|
|
318
|
+
<p className="pb-2 text-xs leading-5 text-faint">
|
|
319
|
+
Generated at {formatGeneratedAt(manifest.generatedAt)}
|
|
320
|
+
</p>
|
|
321
|
+
<p className="pb-5 text-xs font-medium leading-5 text-muted">
|
|
322
|
+
Built using{" "}
|
|
323
|
+
<a
|
|
324
|
+
target="_blank"
|
|
325
|
+
rel="noreferrer"
|
|
326
|
+
href="https://messagevisor.com"
|
|
327
|
+
className="font-semibold hover:underline"
|
|
328
|
+
>
|
|
329
|
+
Messagevisor
|
|
330
|
+
</a>
|
|
331
|
+
</p>
|
|
332
|
+
</footer>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</main>
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export function PageHeader(props: { title: string; description?: ReactNode; actions?: ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="mb-6 flex flex-col justify-between gap-4 border-b border-border px-6 pb-4 pt-8 md:flex-row md:items-start">
|
|
6
|
+
<div className="min-w-0 flex-1">
|
|
7
|
+
<h1 className="break-words text-3xl font-black text-text">{props.title}</h1>
|
|
8
|
+
{props.description && <div className="mt-2 text-sm text-muted">{props.description}</div>}
|
|
9
|
+
</div>
|
|
10
|
+
{props.actions ? <div className="shrink-0">{props.actions}</div> : null}
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { NavLink } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
export interface TabItem {
|
|
5
|
+
label: string;
|
|
6
|
+
to: string;
|
|
7
|
+
end?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Tabs(props: { tabs: TabItem[]; children: ReactNode }) {
|
|
11
|
+
return (
|
|
12
|
+
<div>
|
|
13
|
+
<nav className="border-b border-border">
|
|
14
|
+
{props.tabs.map((tab) => (
|
|
15
|
+
<NavLink
|
|
16
|
+
key={tab.to}
|
|
17
|
+
to={tab.to}
|
|
18
|
+
end={tab.end}
|
|
19
|
+
className={({ isActive }) =>
|
|
20
|
+
[
|
|
21
|
+
"inline-block min-w-28 border-b-2 px-3 pb-4 pt-2 text-center text-sm font-medium",
|
|
22
|
+
isActive
|
|
23
|
+
? "border-primary text-primary"
|
|
24
|
+
: "border-transparent text-muted hover:border-border hover:text-text",
|
|
25
|
+
].join(" ")
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
{tab.label}
|
|
29
|
+
</NavLink>
|
|
30
|
+
))}
|
|
31
|
+
</nav>
|
|
32
|
+
<div className="px-6 py-6">{props.children}</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|