@nexub/patterns 1.6.0 → 1.8.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/dist/{chunk-ZV73WS4X.js → chunk-5IORYY2L.js} +1 -0
- package/dist/{chunk-LAYYOVIK.js → chunk-6PV4NM55.js} +38 -22
- package/dist/{chunk-JTIMCXPS.js → chunk-HYC5N5MO.js} +0 -1
- package/dist/{chunk-K5EFLRKU.js → chunk-INKX2GYA.js} +4 -1
- package/dist/components/ActivityFeed/index.js +1 -1
- package/dist/components/AppHeader/index.js +1 -1
- package/dist/components/NotificationItem/index.js +1 -1
- package/dist/components/StatCard/index.js +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.js +15 -15
- package/package.json +7 -7
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
Logo,
|
|
15
15
|
TopNav,
|
|
16
16
|
cn,
|
|
17
|
+
useNexubMessages,
|
|
17
18
|
useTheme
|
|
18
19
|
} from "@nexub/react";
|
|
19
20
|
import { Building2, ChevronDown, Moon, Search, Sun } from "@nexub/icons";
|
|
@@ -26,6 +27,7 @@ function HeaderSearch({
|
|
|
26
27
|
items,
|
|
27
28
|
onSearch
|
|
28
29
|
}) {
|
|
30
|
+
const t = useNexubMessages();
|
|
29
31
|
const [open, setOpen] = React.useState(false);
|
|
30
32
|
const [query, setQuery] = React.useState("");
|
|
31
33
|
const [results, setResults] = React.useState([]);
|
|
@@ -88,11 +90,7 @@ function HeaderSearch({
|
|
|
88
90
|
"aria-haspopup": "dialog"
|
|
89
91
|
};
|
|
90
92
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
91
|
-
/* @__PURE__ */
|
|
92
|
-
"Keyboard shortcut: ",
|
|
93
|
-
isMac ? "Command" : "Control",
|
|
94
|
-
" K"
|
|
95
|
-
] }),
|
|
93
|
+
/* @__PURE__ */ jsx("span", { id: hintId, className: "sr-only", children: t.keyboardShortcut(`${isMac ? t.commandKey : t.controlKey} K`) }),
|
|
96
94
|
/* @__PURE__ */ jsxs(
|
|
97
95
|
"button",
|
|
98
96
|
{
|
|
@@ -118,8 +116,8 @@ function HeaderSearch({
|
|
|
118
116
|
children: [
|
|
119
117
|
/* @__PURE__ */ jsx(CommandPalette.Input, { placeholder, value: query, onValueChange: setQuery }),
|
|
120
118
|
/* @__PURE__ */ jsxs(CommandPalette.List, { children: [
|
|
121
|
-
loading && /* @__PURE__ */ jsx("div", { role: "status", className: "px-3 py-2 text-sm text-[var(--color-on-surface-muted)]", children:
|
|
122
|
-
!loading && /* @__PURE__ */ jsx(CommandPalette.Empty, { children: failed ?
|
|
119
|
+
loading && /* @__PURE__ */ jsx("div", { role: "status", className: "px-3 py-2 text-sm text-[var(--color-on-surface-muted)]", children: t.searching }),
|
|
120
|
+
!loading && /* @__PURE__ */ jsx(CommandPalette.Empty, { children: failed ? t.searchFailed : query ? t.noResultsFor(query) : t.typeToSearch }),
|
|
123
121
|
groups.map((group) => /* @__PURE__ */ jsx(CommandPalette.Group, { heading: group.label, children: group.items.map((item) => /* @__PURE__ */ jsxs(
|
|
124
122
|
CommandPalette.Item,
|
|
125
123
|
{
|
|
@@ -149,6 +147,7 @@ function OrgSwitcher({
|
|
|
149
147
|
currentOrgId,
|
|
150
148
|
onOrgChange
|
|
151
149
|
}) {
|
|
150
|
+
const t = useNexubMessages();
|
|
152
151
|
const current = orgs.find((o) => o.id === currentOrgId) ?? orgs[0];
|
|
153
152
|
if (!current) return null;
|
|
154
153
|
if (orgs.length === 1) {
|
|
@@ -161,7 +160,7 @@ function OrgSwitcher({
|
|
|
161
160
|
"button",
|
|
162
161
|
{
|
|
163
162
|
type: "button",
|
|
164
|
-
"aria-label":
|
|
163
|
+
"aria-label": t.switchOrganization(current.name),
|
|
165
164
|
title: current.name,
|
|
166
165
|
className: "nx-state-layer inline-flex h-9 min-w-0 shrink-0 items-center gap-1 rounded-shape-control px-2 text-sm sm:shrink font-medium text-[var(--color-on-surface)]",
|
|
167
166
|
children: [
|
|
@@ -172,24 +171,32 @@ function OrgSwitcher({
|
|
|
172
171
|
}
|
|
173
172
|
) }),
|
|
174
173
|
/* @__PURE__ */ jsx(DropdownMenu.Portal, { children: /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", className: "max-w-[calc(100vw-1rem)]", children: [
|
|
175
|
-
/* @__PURE__ */ jsx(DropdownMenu.Label, { children:
|
|
174
|
+
/* @__PURE__ */ jsx(DropdownMenu.Label, { children: t.organizations }),
|
|
176
175
|
/* @__PURE__ */ jsx(DropdownMenu.RadioGroup, { value: current.id, onValueChange: (id) => onOrgChange?.(id), children: orgs.map((org) => /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: org.id, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: org.name }) }, org.id)) })
|
|
177
176
|
] }) })
|
|
178
177
|
] })
|
|
179
178
|
);
|
|
180
179
|
}
|
|
181
180
|
function ThemeSwitch() {
|
|
181
|
+
const t = useNexubMessages();
|
|
182
182
|
const { resolvedTheme, setTheme } = useTheme();
|
|
183
183
|
const dark = resolvedTheme === "dark";
|
|
184
|
-
return /* @__PURE__ */ jsx(IconButton, { label: dark ?
|
|
184
|
+
return /* @__PURE__ */ jsx(IconButton, { label: dark ? t.switchToLightTheme : t.switchToDarkTheme, onClick: () => setTheme(dark ? "light" : "dark"), children: dark ? /* @__PURE__ */ jsx(Sun, { size: "md", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Moon, { size: "md", "aria-hidden": "true" }) });
|
|
185
185
|
}
|
|
186
|
-
var
|
|
186
|
+
var GLASS_MODES = ["clear", "balanced", "tinted", "off"];
|
|
187
187
|
function GlassItems() {
|
|
188
|
+
const t = useNexubMessages();
|
|
188
189
|
const { glass, setGlass } = useTheme();
|
|
190
|
+
const labels = {
|
|
191
|
+
clear: t.glassClear,
|
|
192
|
+
balanced: t.glassBalanced,
|
|
193
|
+
tinted: t.glassTinted,
|
|
194
|
+
off: t.glassSolid
|
|
195
|
+
};
|
|
189
196
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
190
197
|
/* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
|
|
191
|
-
/* @__PURE__ */ jsx(DropdownMenu.Label, { children:
|
|
192
|
-
/* @__PURE__ */ jsx(DropdownMenu.RadioGroup, { value: glass, onValueChange: (v) => setGlass(v), children:
|
|
198
|
+
/* @__PURE__ */ jsx(DropdownMenu.Label, { children: t.glass }),
|
|
199
|
+
/* @__PURE__ */ jsx(DropdownMenu.RadioGroup, { value: glass, onValueChange: (v) => setGlass(v), children: GLASS_MODES.map((mode) => /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: mode, onSelect: (e) => e.preventDefault(), children: labels[mode] }, mode)) })
|
|
193
200
|
] });
|
|
194
201
|
}
|
|
195
202
|
function AccountMenu({
|
|
@@ -201,6 +208,7 @@ function AccountMenu({
|
|
|
201
208
|
accountMenuItems,
|
|
202
209
|
glassControl
|
|
203
210
|
}) {
|
|
211
|
+
const t = useNexubMessages();
|
|
204
212
|
const [open, setOpen] = React.useState(false);
|
|
205
213
|
const [signingOut, setSigningOut] = React.useState(false);
|
|
206
214
|
const signOut = (e) => {
|
|
@@ -214,23 +222,23 @@ function AccountMenu({
|
|
|
214
222
|
};
|
|
215
223
|
const link = (href, label) => /* @__PURE__ */ jsx(DropdownMenu.Item, { asChild: true, children: /* @__PURE__ */ jsx("a", { href, children: label }) });
|
|
216
224
|
const legal = (privacyHref || termsHref) && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
217
|
-
privacyHref && link(privacyHref,
|
|
218
|
-
termsHref && link(termsHref,
|
|
225
|
+
privacyHref && link(privacyHref, t.privacyPolicy),
|
|
226
|
+
termsHref && link(termsHref, t.termsOfService)
|
|
219
227
|
] });
|
|
220
228
|
const last = (accountMenuItems || onSignOut) && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
221
229
|
accountMenuItems,
|
|
222
|
-
onSignOut && /* @__PURE__ */ jsx(DropdownMenu.Item, { disabled: signingOut, onSelect: signOut, children: signingOut ?
|
|
230
|
+
onSignOut && /* @__PURE__ */ jsx(DropdownMenu.Item, { disabled: signingOut, onSelect: signOut, children: signingOut ? t.signingOut : t.signOut })
|
|
223
231
|
] });
|
|
224
232
|
return (
|
|
225
233
|
// modal={false}: Radix's modal menu injects a <style> scroll lock (strict CSP).
|
|
226
234
|
/* @__PURE__ */ jsxs(DropdownMenu.Root, { open, onOpenChange: setOpen, modal: false, children: [
|
|
227
|
-
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", "aria-label":
|
|
235
|
+
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", "aria-label": t.accountMenu(user.name), className: "ms-1 shrink-0 rounded-full", children: /* @__PURE__ */ jsx(Avatar, { name: user.name, src: user.avatarUrl, size: "sm", "aria-hidden": "true" }) }) }),
|
|
228
236
|
/* @__PURE__ */ jsx(DropdownMenu.Portal, { children: /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", className: "w-72 max-w-[calc(100vw-1rem)]", children: [
|
|
229
237
|
/* @__PURE__ */ jsxs(DropdownMenu.Label, { className: "flex flex-col gap-0.5 py-2", children: [
|
|
230
238
|
/* @__PURE__ */ jsx("span", { className: "truncate text-title-sm text-[var(--color-on-surface)]", children: user.name }),
|
|
231
239
|
user.email && /* @__PURE__ */ jsx("span", { className: "truncate text-body-sm text-[var(--color-on-surface-muted)]", children: user.email })
|
|
232
240
|
] }),
|
|
233
|
-
accountHref && link(accountHref,
|
|
241
|
+
accountHref && link(accountHref, t.manageAccount),
|
|
234
242
|
glassControl && /* @__PURE__ */ jsx(GlassItems, {}),
|
|
235
243
|
legal && /* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
|
|
236
244
|
legal,
|
|
@@ -262,10 +270,12 @@ var AppHeader = React.forwardRef(
|
|
|
262
270
|
accountMenuItems,
|
|
263
271
|
surface = "solid",
|
|
264
272
|
condense = false,
|
|
273
|
+
position = "fixed",
|
|
265
274
|
className,
|
|
266
275
|
...props
|
|
267
276
|
}, ref) => {
|
|
268
|
-
const
|
|
277
|
+
const t = useNexubMessages();
|
|
278
|
+
const placeholder = searchPlaceholder ?? (productName ? t.searchProduct(productName) : t.search);
|
|
269
279
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
270
280
|
/* @__PURE__ */ jsx(
|
|
271
281
|
TopNav.Root,
|
|
@@ -274,7 +284,13 @@ var AppHeader = React.forwardRef(
|
|
|
274
284
|
role: "banner",
|
|
275
285
|
surface,
|
|
276
286
|
"data-condense": condense ? "" : void 0,
|
|
277
|
-
className: cn(
|
|
287
|
+
className: cn(
|
|
288
|
+
"h-[var(--app-header-height,64px)] px-2 sm:px-4",
|
|
289
|
+
// TopNav.Root is sticky by default.
|
|
290
|
+
position === "fixed" && "fixed inset-x-0 top-0",
|
|
291
|
+
position === "static" && "static",
|
|
292
|
+
className
|
|
293
|
+
),
|
|
278
294
|
...props,
|
|
279
295
|
children: /* @__PURE__ */ jsxs("div", { className: "flex w-full min-w-0 items-center gap-1 sm:gap-2", children: [
|
|
280
296
|
menu && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 md:hidden", children: menu }),
|
|
@@ -293,7 +309,7 @@ var AppHeader = React.forwardRef(
|
|
|
293
309
|
}
|
|
294
310
|
),
|
|
295
311
|
/* @__PURE__ */ jsx("div", { className: "ms-auto flex shrink-0 md:ms-0 md:min-w-0 md:flex-1 md:justify-center md:px-2 lg:px-4", children: (onSearch || searchItems) && /* @__PURE__ */ jsx(HeaderSearch, { placeholder, items: searchItems, onSearch }) }),
|
|
296
|
-
/* @__PURE__ */ jsxs("nav", { "aria-label":
|
|
312
|
+
/* @__PURE__ */ jsxs("nav", { "aria-label": t.accountAndApps, className: "flex shrink-0 items-center gap-0.5 sm:gap-1", children: [
|
|
297
313
|
orgs.length > 0 && /* @__PURE__ */ jsx(OrgSwitcher, { orgs, currentOrgId, onOrgChange }),
|
|
298
314
|
showThemeSwitch && /* @__PURE__ */ jsx(ThemeSwitch, {}),
|
|
299
315
|
apps && /* @__PURE__ */ jsx(AppLauncher, { apps, surface, onNavigate: onNavigate && ((app) => onNavigate(app.href)) }),
|
|
@@ -313,7 +329,7 @@ var AppHeader = React.forwardRef(
|
|
|
313
329
|
] })
|
|
314
330
|
}
|
|
315
331
|
),
|
|
316
|
-
/* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "h-[var(--app-header-height,64px)] shrink-0" })
|
|
332
|
+
position === "fixed" && /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "h-[var(--app-header-height,64px)] shrink-0" })
|
|
317
333
|
] });
|
|
318
334
|
}
|
|
319
335
|
);
|
|
@@ -11,13 +11,15 @@ var StatCard = React.forwardRef(
|
|
|
11
11
|
value,
|
|
12
12
|
delta,
|
|
13
13
|
deltaType = "neutral",
|
|
14
|
+
sentiment,
|
|
14
15
|
icon,
|
|
15
16
|
description,
|
|
16
17
|
animate = false,
|
|
17
18
|
className,
|
|
18
19
|
...props
|
|
19
20
|
}, ref) => {
|
|
20
|
-
const
|
|
21
|
+
const tone = sentiment ?? (deltaType === "increase" ? "positive" : deltaType === "decrease" ? "negative" : "neutral");
|
|
22
|
+
const deltaColor = tone === "positive" ? "text-[var(--color-success)]" : tone === "negative" ? "text-[var(--color-error)]" : "text-[var(--color-on-surface-muted)]";
|
|
21
23
|
const DeltaIcon = deltaType === "increase" ? TrendingUp : deltaType === "decrease" ? TrendingDown : Minus;
|
|
22
24
|
return /* @__PURE__ */ jsxs(
|
|
23
25
|
"div",
|
|
@@ -40,6 +42,7 @@ var StatCard = React.forwardRef(
|
|
|
40
42
|
{
|
|
41
43
|
className: cn("inline-flex items-center gap-1 text-xs font-medium", deltaColor),
|
|
42
44
|
"data-delta-type": deltaType,
|
|
45
|
+
"data-sentiment": tone,
|
|
43
46
|
children: [
|
|
44
47
|
/* @__PURE__ */ jsx(DeltaIcon, { size: "xs", "aria-hidden": "true" }),
|
|
45
48
|
delta
|
package/dist/index.d.ts
CHANGED
|
@@ -29,7 +29,14 @@ interface StatCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
29
29
|
label: string;
|
|
30
30
|
value: React.ReactNode;
|
|
31
31
|
delta?: string;
|
|
32
|
+
/** Direction of the change: sets the arrow (and, by default, the colour). */
|
|
32
33
|
deltaType?: 'increase' | 'decrease' | 'neutral';
|
|
34
|
+
/**
|
|
35
|
+
* Whether the change is good or bad, independent of its direction — e.g. churn going
|
|
36
|
+
* down is `deltaType="decrease"` + `sentiment="positive"`.
|
|
37
|
+
* @default derived from `deltaType` (increase → positive, decrease → negative)
|
|
38
|
+
*/
|
|
39
|
+
sentiment?: 'positive' | 'negative' | 'neutral';
|
|
33
40
|
icon?: React.ReactNode;
|
|
34
41
|
description?: string;
|
|
35
42
|
/** Roll digits when a number/string `value` changes (NumberTicker); announced politely. */
|
|
@@ -268,10 +275,17 @@ interface AppHeaderProps extends Omit<React.HTMLAttributes<HTMLElement>, 'childr
|
|
|
268
275
|
scrollEdge?: 'soft' | 'hard';
|
|
269
276
|
/** Shrink 64 → 52px and lift onto elevation 1 over the first 64px of page scroll (CSS scroll timeline; static where unsupported). */
|
|
270
277
|
condense?: boolean;
|
|
278
|
+
/**
|
|
279
|
+
* `fixed` pins it to the viewport and renders a spacer after it. `sticky` and `static`
|
|
280
|
+
* keep it in flow (no spacer), for layouts, embeds and previews.
|
|
281
|
+
* @default 'fixed'
|
|
282
|
+
*/
|
|
283
|
+
position?: 'fixed' | 'sticky' | 'static';
|
|
271
284
|
}
|
|
272
285
|
/**
|
|
273
|
-
* The one top header every Nexub app shares. Fixed to the top
|
|
274
|
-
* spacer of `--app-header-height` after itself, so content starts
|
|
286
|
+
* The one top header every Nexub app shares. Fixed to the top by default and
|
|
287
|
+
* renders a spacer of `--app-header-height` after itself, so content starts
|
|
288
|
+
* below it; `position="sticky" | "static"` keeps it in flow without a spacer.
|
|
275
289
|
*/
|
|
276
290
|
declare const AppHeader: React.ForwardRefExoticComponent<AppHeaderProps & React.RefAttributes<HTMLElement>>;
|
|
277
291
|
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
UserCard
|
|
4
|
+
} from "./chunk-7NYGYETG.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
ErrorSummary,
|
|
7
|
+
useFormErrors
|
|
8
|
+
} from "./chunk-RWPFCNMJ.js";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
InfoList
|
|
11
|
+
} from "./chunk-WPNT5JOU.js";
|
|
11
12
|
import {
|
|
12
13
|
ListDetail
|
|
13
14
|
} from "./chunk-5JU6HUVJ.js";
|
|
14
15
|
import {
|
|
15
16
|
NotificationItem
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-5IORYY2L.js";
|
|
17
18
|
import {
|
|
18
19
|
PlanCard
|
|
19
20
|
} from "./chunk-YB7G4YBU.js";
|
|
@@ -26,16 +27,16 @@ import {
|
|
|
26
27
|
} from "./chunk-U3FT7XAU.js";
|
|
27
28
|
import {
|
|
28
29
|
StatCard
|
|
29
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-INKX2GYA.js";
|
|
30
31
|
import {
|
|
31
|
-
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
StepIndicator
|
|
33
|
+
} from "./chunk-L456OB3Q.js";
|
|
33
34
|
import {
|
|
34
35
|
ActivityFeed
|
|
35
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-HYC5N5MO.js";
|
|
36
37
|
import {
|
|
37
38
|
AppHeader
|
|
38
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-6PV4NM55.js";
|
|
39
40
|
import "./chunk-VYMTTYPR.js";
|
|
40
41
|
import {
|
|
41
42
|
AppLauncher
|
|
@@ -51,9 +52,8 @@ import {
|
|
|
51
52
|
Conversation
|
|
52
53
|
} from "./chunk-WRP73TXG.js";
|
|
53
54
|
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} from "./chunk-RWPFCNMJ.js";
|
|
55
|
+
FilterBar
|
|
56
|
+
} from "./chunk-Z6VMCMSW.js";
|
|
57
57
|
export {
|
|
58
58
|
ActivityFeed,
|
|
59
59
|
AppHeader,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexub/patterns",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Composite UI patterns for Nexub Design System — pre-built, production-ready layouts and widgets built on @nexub/react.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": ">=18 <20",
|
|
38
38
|
"react-dom": ">=18 <20",
|
|
39
|
-
"@nexub/icons": "^1.
|
|
40
|
-
"@nexub/react": "^1.
|
|
41
|
-
"@nexub/tokens": "^1.
|
|
39
|
+
"@nexub/icons": "^1.8.0",
|
|
40
|
+
"@nexub/react": "^1.8.0",
|
|
41
|
+
"@nexub/tokens": "^1.8.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"@nexub/icons": {
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"typescript": "^5.9.3",
|
|
60
60
|
"vite": "^7.3.7",
|
|
61
61
|
"vitest": "^5.0.5",
|
|
62
|
-
"@nexub/icons": "1.
|
|
63
|
-
"@nexub/react": "1.
|
|
64
|
-
"@nexub/tokens": "1.
|
|
62
|
+
"@nexub/icons": "1.8.0",
|
|
63
|
+
"@nexub/react": "1.8.0",
|
|
64
|
+
"@nexub/tokens": "1.8.0"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"build": "tsup",
|