@frontmcp/ui 0.8.1 → 0.10.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/README.md +67 -188
- package/bridge/adapters/claude.adapter.d.ts.map +1 -1
- package/bridge/adapters/gemini.adapter.d.ts.map +1 -1
- package/bridge/index.js +16 -4
- package/components/alert.d.ts +20 -3
- package/components/alert.d.ts.map +1 -1
- package/components/badge.d.ts +16 -2
- package/components/badge.d.ts.map +1 -1
- package/components/card.d.ts +29 -6
- package/components/card.d.ts.map +1 -1
- package/components/form.d.ts +21 -6
- package/components/form.d.ts.map +1 -1
- package/components/index.js +83 -30
- package/esm/bridge/index.mjs +16 -4
- package/esm/components/index.mjs +83 -30
- package/esm/index.mjs +94 -31
- package/esm/package.json +3 -3
- package/esm/renderers/index.mjs +11 -1
- package/esm/universal/index.mjs +2 -7
- package/esm/web-components/index.mjs +52 -22
- package/index.js +94 -31
- package/package.json +3 -3
- package/renderers/index.js +11 -1
- package/renderers/react.adapter.d.ts.map +1 -1
- package/universal/index.js +4 -9
- package/universal/renderers/html.renderer.d.ts +7 -8
- package/universal/renderers/html.renderer.d.ts.map +1 -1
- package/web-components/index.js +52 -22
package/components/index.js
CHANGED
|
@@ -984,6 +984,7 @@ var dangerButton = (text, opts) => button(text, { ...opts, variant: "danger" });
|
|
|
984
984
|
var linkButton = (text, opts) => button(text, { ...opts, variant: "link" });
|
|
985
985
|
|
|
986
986
|
// libs/ui/src/components/card.ts
|
|
987
|
+
var import_runtime = require("@frontmcp/uipack/runtime");
|
|
987
988
|
function getVariantClasses2(variant) {
|
|
988
989
|
const variants = {
|
|
989
990
|
default: "bg-white border border-border rounded-xl shadow-sm",
|
|
@@ -1018,33 +1019,38 @@ function card(content, options = {}) {
|
|
|
1018
1019
|
id,
|
|
1019
1020
|
data,
|
|
1020
1021
|
clickable = false,
|
|
1021
|
-
href
|
|
1022
|
+
href,
|
|
1023
|
+
sanitize = false
|
|
1022
1024
|
} = options;
|
|
1025
|
+
const safeContent = sanitize ? (0, import_runtime.sanitizeHtmlContent)(content) : content;
|
|
1026
|
+
const safeHeaderActions = sanitize && headerActions ? (0, import_runtime.sanitizeHtmlContent)(headerActions) : headerActions;
|
|
1027
|
+
const safeFooter = sanitize && footer ? (0, import_runtime.sanitizeHtmlContent)(footer) : footer;
|
|
1023
1028
|
const variantClasses = getVariantClasses2(variant);
|
|
1024
1029
|
const sizeClasses = getSizeClasses2(size);
|
|
1025
1030
|
const clickableClasses = clickable ? "cursor-pointer hover:shadow-md transition-shadow" : "";
|
|
1026
|
-
const
|
|
1031
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1032
|
+
const allClasses = [variantClasses, sizeClasses, clickableClasses, safeClassName].filter(Boolean).join(" ");
|
|
1027
1033
|
const dataAttrs = buildDataAttrs(data);
|
|
1028
1034
|
const idAttr = id ? `id="${(0, import_utils2.escapeHtml)(id)}"` : "";
|
|
1029
|
-
const hasHeader = title || subtitle ||
|
|
1035
|
+
const hasHeader = title || subtitle || safeHeaderActions;
|
|
1030
1036
|
const headerHtml = hasHeader ? `<div class="flex items-start justify-between mb-4">
|
|
1031
1037
|
<div>
|
|
1032
1038
|
${title ? `<h3 class="text-lg font-semibold text-text-primary">${(0, import_utils2.escapeHtml)(title)}</h3>` : ""}
|
|
1033
1039
|
${subtitle ? `<p class="text-sm text-text-secondary mt-1">${(0, import_utils2.escapeHtml)(subtitle)}</p>` : ""}
|
|
1034
1040
|
</div>
|
|
1035
|
-
${
|
|
1041
|
+
${safeHeaderActions ? `<div class="flex items-center gap-2">${safeHeaderActions}</div>` : ""}
|
|
1036
1042
|
</div>` : "";
|
|
1037
|
-
const footerHtml =
|
|
1043
|
+
const footerHtml = safeFooter ? `<div class="mt-4 pt-4 border-t border-divider">${safeFooter}</div>` : "";
|
|
1038
1044
|
if (href) {
|
|
1039
1045
|
return `<a href="${(0, import_utils2.escapeHtml)(href)}" class="${allClasses}" ${idAttr} ${dataAttrs}>
|
|
1040
1046
|
${headerHtml}
|
|
1041
|
-
${
|
|
1047
|
+
${safeContent}
|
|
1042
1048
|
${footerHtml}
|
|
1043
1049
|
</a>`;
|
|
1044
1050
|
}
|
|
1045
1051
|
return `<div class="${allClasses}" ${idAttr} ${dataAttrs}>
|
|
1046
1052
|
${headerHtml}
|
|
1047
|
-
${
|
|
1053
|
+
${safeContent}
|
|
1048
1054
|
${footerHtml}
|
|
1049
1055
|
</div>`;
|
|
1050
1056
|
}
|
|
@@ -1052,12 +1058,14 @@ function cardGroup(cards, options = {}) {
|
|
|
1052
1058
|
const { direction = "vertical", gap = "md", className = "" } = options;
|
|
1053
1059
|
const gapClasses = { sm: "gap-2", md: "gap-4", lg: "gap-6" };
|
|
1054
1060
|
const directionClasses = direction === "horizontal" ? "flex flex-row flex-wrap" : "flex flex-col";
|
|
1055
|
-
|
|
1061
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1062
|
+
return `<div class="${directionClasses} ${gapClasses[gap]} ${safeClassName}">
|
|
1056
1063
|
${cards.join("\n")}
|
|
1057
1064
|
</div>`;
|
|
1058
1065
|
}
|
|
1059
1066
|
|
|
1060
1067
|
// libs/ui/src/components/form.ts
|
|
1068
|
+
var import_runtime2 = require("@frontmcp/uipack/runtime");
|
|
1061
1069
|
function getInputSizeClasses(size) {
|
|
1062
1070
|
const sizes = {
|
|
1063
1071
|
sm: "px-3 py-1.5 text-sm",
|
|
@@ -1102,11 +1110,15 @@ function input(options) {
|
|
|
1102
1110
|
className = "",
|
|
1103
1111
|
data,
|
|
1104
1112
|
iconBefore,
|
|
1105
|
-
iconAfter
|
|
1113
|
+
iconAfter,
|
|
1114
|
+
sanitize = false
|
|
1106
1115
|
} = options;
|
|
1116
|
+
const safeIconBefore = sanitize && iconBefore ? (0, import_runtime2.sanitizeHtmlContent)(iconBefore) : iconBefore;
|
|
1117
|
+
const safeIconAfter = sanitize && iconAfter ? (0, import_runtime2.sanitizeHtmlContent)(iconAfter) : iconAfter;
|
|
1107
1118
|
const sizeClasses = getInputSizeClasses(size);
|
|
1108
1119
|
const stateClasses = getInputStateClasses(state);
|
|
1109
|
-
const hasIcon =
|
|
1120
|
+
const hasIcon = safeIconBefore || safeIconAfter;
|
|
1121
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1110
1122
|
const baseClasses = [
|
|
1111
1123
|
"w-full rounded-lg border bg-white",
|
|
1112
1124
|
"transition-colors duration-200",
|
|
@@ -1114,8 +1126,8 @@ function input(options) {
|
|
|
1114
1126
|
disabled ? "opacity-50 cursor-not-allowed bg-gray-50" : "",
|
|
1115
1127
|
sizeClasses,
|
|
1116
1128
|
stateClasses,
|
|
1117
|
-
hasIcon ? (
|
|
1118
|
-
|
|
1129
|
+
hasIcon ? (safeIconBefore ? "pl-10" : "") + (safeIconAfter ? " pr-10" : "") : "",
|
|
1130
|
+
safeClassName
|
|
1119
1131
|
].filter(Boolean).join(" ");
|
|
1120
1132
|
const dataAttrs = buildDataAttrs2(data);
|
|
1121
1133
|
const inputAttrs = [
|
|
@@ -1140,8 +1152,8 @@ function input(options) {
|
|
|
1140
1152
|
</label>` : "";
|
|
1141
1153
|
const helperHtml = helper && !error ? `<p class="mt-1.5 text-sm text-text-secondary">${(0, import_utils2.escapeHtml)(helper)}</p>` : "";
|
|
1142
1154
|
const errorHtml = error ? `<p class="mt-1.5 text-sm text-danger">${(0, import_utils2.escapeHtml)(error)}</p>` : "";
|
|
1143
|
-
const iconBeforeHtml =
|
|
1144
|
-
const iconAfterHtml =
|
|
1155
|
+
const iconBeforeHtml = safeIconBefore ? `<span class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary">${safeIconBefore}</span>` : "";
|
|
1156
|
+
const iconAfterHtml = safeIconAfter ? `<span class="absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary">${safeIconAfter}</span>` : "";
|
|
1145
1157
|
const inputHtml = hasIcon ? `<div class="relative">
|
|
1146
1158
|
${iconBeforeHtml}
|
|
1147
1159
|
<input ${inputAttrs}>
|
|
@@ -1173,6 +1185,7 @@ function select(options) {
|
|
|
1173
1185
|
} = options;
|
|
1174
1186
|
const sizeClasses = getInputSizeClasses(size);
|
|
1175
1187
|
const stateClasses = getInputStateClasses(state);
|
|
1188
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1176
1189
|
const baseClasses = [
|
|
1177
1190
|
"w-full rounded-lg border bg-white",
|
|
1178
1191
|
"transition-colors duration-200",
|
|
@@ -1180,7 +1193,7 @@ function select(options) {
|
|
|
1180
1193
|
disabled ? "opacity-50 cursor-not-allowed bg-gray-50" : "",
|
|
1181
1194
|
sizeClasses,
|
|
1182
1195
|
stateClasses,
|
|
1183
|
-
|
|
1196
|
+
safeClassName
|
|
1184
1197
|
].filter(Boolean).join(" ");
|
|
1185
1198
|
const dataAttrs = buildDataAttrs2(data);
|
|
1186
1199
|
const optionsHtml = selectOptions.map((opt) => {
|
|
@@ -1237,6 +1250,7 @@ function textarea(options) {
|
|
|
1237
1250
|
horizontal: "resize-x",
|
|
1238
1251
|
both: "resize"
|
|
1239
1252
|
};
|
|
1253
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1240
1254
|
const baseClasses = [
|
|
1241
1255
|
"w-full rounded-lg border bg-white",
|
|
1242
1256
|
"transition-colors duration-200",
|
|
@@ -1245,7 +1259,7 @@ function textarea(options) {
|
|
|
1245
1259
|
sizeClasses,
|
|
1246
1260
|
stateClasses,
|
|
1247
1261
|
resizeClasses[resize],
|
|
1248
|
-
|
|
1262
|
+
safeClassName
|
|
1249
1263
|
].filter(Boolean).join(" ");
|
|
1250
1264
|
const dataAttrs = buildDataAttrs2(data);
|
|
1251
1265
|
const labelHtml = label ? `<label for="${(0, import_utils2.escapeHtml)(id)}" class="block text-sm font-medium text-text-primary mb-1.5">
|
|
@@ -1289,7 +1303,8 @@ function checkbox(options) {
|
|
|
1289
1303
|
].join(" ");
|
|
1290
1304
|
const helperHtml = helper && !error ? `<p class="text-sm text-text-secondary">${(0, import_utils2.escapeHtml)(helper)}</p>` : "";
|
|
1291
1305
|
const errorHtml = error ? `<p class="text-sm text-danger">${(0, import_utils2.escapeHtml)(error)}</p>` : "";
|
|
1292
|
-
|
|
1306
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1307
|
+
return `<div class="form-field ${safeClassName}">
|
|
1293
1308
|
<label class="flex items-start gap-3 ${disabled ? "cursor-not-allowed" : "cursor-pointer"}">
|
|
1294
1309
|
<input
|
|
1295
1310
|
type="checkbox"
|
|
@@ -1333,7 +1348,8 @@ function radioGroup(options) {
|
|
|
1333
1348
|
const labelHtml = label ? `<label class="block text-sm font-medium text-text-primary mb-2">${(0, import_utils2.escapeHtml)(label)}</label>` : "";
|
|
1334
1349
|
const helperHtml = helper && !error ? `<p class="mt-1.5 text-sm text-text-secondary">${(0, import_utils2.escapeHtml)(helper)}</p>` : "";
|
|
1335
1350
|
const errorHtml = error ? `<p class="mt-1.5 text-sm text-danger">${(0, import_utils2.escapeHtml)(error)}</p>` : "";
|
|
1336
|
-
|
|
1351
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1352
|
+
return `<div class="form-field ${safeClassName}" role="radiogroup">
|
|
1337
1353
|
${labelHtml}
|
|
1338
1354
|
<div class="${directionClasses}">
|
|
1339
1355
|
${radiosHtml}
|
|
@@ -1355,10 +1371,26 @@ function form(content, options = {}) {
|
|
|
1355
1371
|
].filter(Boolean).join(" ");
|
|
1356
1372
|
return `<form ${attrs}>${content}</form>`;
|
|
1357
1373
|
}
|
|
1374
|
+
var GRID_COLS_MAP = {
|
|
1375
|
+
1: "grid-cols-1",
|
|
1376
|
+
2: "grid-cols-2",
|
|
1377
|
+
3: "grid-cols-3",
|
|
1378
|
+
4: "grid-cols-4",
|
|
1379
|
+
5: "grid-cols-5",
|
|
1380
|
+
6: "grid-cols-6",
|
|
1381
|
+
7: "grid-cols-7",
|
|
1382
|
+
8: "grid-cols-8",
|
|
1383
|
+
9: "grid-cols-9",
|
|
1384
|
+
10: "grid-cols-10",
|
|
1385
|
+
11: "grid-cols-11",
|
|
1386
|
+
12: "grid-cols-12"
|
|
1387
|
+
};
|
|
1358
1388
|
function formRow(fields, options = {}) {
|
|
1359
1389
|
const { gap = "md", className = "" } = options;
|
|
1360
1390
|
const gapClasses = { sm: "gap-2", md: "gap-4", lg: "gap-6" };
|
|
1361
|
-
|
|
1391
|
+
const gridCols = GRID_COLS_MAP[fields.length] ?? "grid-cols-1";
|
|
1392
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1393
|
+
return `<div class="grid ${gridCols} ${gapClasses[gap]} ${safeClassName}">
|
|
1362
1394
|
${fields.join("\n")}
|
|
1363
1395
|
</div>`;
|
|
1364
1396
|
}
|
|
@@ -1368,7 +1400,8 @@ function formSection(content, options = {}) {
|
|
|
1368
1400
|
<h3 class="text-lg font-semibold text-text-primary">${(0, import_utils2.escapeHtml)(title)}</h3>
|
|
1369
1401
|
${description ? `<p class="text-sm text-text-secondary mt-1">${(0, import_utils2.escapeHtml)(description)}</p>` : ""}
|
|
1370
1402
|
</div>` : "";
|
|
1371
|
-
|
|
1403
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1404
|
+
return `<div class="form-section ${safeClassName}">
|
|
1372
1405
|
${headerHtml}
|
|
1373
1406
|
<div class="space-y-4">
|
|
1374
1407
|
${content}
|
|
@@ -1383,7 +1416,8 @@ function formActions(buttons, options = {}) {
|
|
|
1383
1416
|
right: "justify-end",
|
|
1384
1417
|
between: "justify-between"
|
|
1385
1418
|
};
|
|
1386
|
-
|
|
1419
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1420
|
+
return `<div class="flex items-center gap-3 pt-4 ${alignClasses[align]} ${safeClassName}">
|
|
1387
1421
|
${buttons.join("\n")}
|
|
1388
1422
|
</div>`;
|
|
1389
1423
|
}
|
|
@@ -1395,6 +1429,7 @@ function csrfInput(token) {
|
|
|
1395
1429
|
}
|
|
1396
1430
|
|
|
1397
1431
|
// libs/ui/src/components/badge.ts
|
|
1432
|
+
var import_runtime3 = require("@frontmcp/uipack/runtime");
|
|
1398
1433
|
function getVariantClasses3(variant) {
|
|
1399
1434
|
const variants = {
|
|
1400
1435
|
default: "bg-gray-100 text-gray-800",
|
|
@@ -1432,8 +1467,11 @@ function badge(text, options = {}) {
|
|
|
1432
1467
|
icon,
|
|
1433
1468
|
dot = false,
|
|
1434
1469
|
className = "",
|
|
1435
|
-
removable = false
|
|
1470
|
+
removable = false,
|
|
1471
|
+
sanitize = false
|
|
1436
1472
|
} = options;
|
|
1473
|
+
const safeIcon = sanitize && icon ? (0, import_runtime3.sanitizeHtmlContent)(icon) : icon;
|
|
1474
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1437
1475
|
if (dot) {
|
|
1438
1476
|
const dotVariants = {
|
|
1439
1477
|
default: "bg-gray-400",
|
|
@@ -1445,7 +1483,7 @@ function badge(text, options = {}) {
|
|
|
1445
1483
|
info: "bg-blue-500",
|
|
1446
1484
|
outline: "border border-current"
|
|
1447
1485
|
};
|
|
1448
|
-
const dotClasses = ["inline-block rounded-full", getSizeClasses3(size, true), dotVariants[variant],
|
|
1486
|
+
const dotClasses = ["inline-block rounded-full", getSizeClasses3(size, true), dotVariants[variant], safeClassName].filter(Boolean).join(" ");
|
|
1449
1487
|
return `<span class="${dotClasses}" aria-label="${(0, import_utils2.escapeHtml)(text)}" title="${(0, import_utils2.escapeHtml)(text)}"></span>`;
|
|
1450
1488
|
}
|
|
1451
1489
|
const variantClasses = getVariantClasses3(variant);
|
|
@@ -1455,9 +1493,9 @@ function badge(text, options = {}) {
|
|
|
1455
1493
|
pill ? "rounded-full" : "rounded-md",
|
|
1456
1494
|
variantClasses,
|
|
1457
1495
|
sizeClasses,
|
|
1458
|
-
|
|
1496
|
+
safeClassName
|
|
1459
1497
|
].filter(Boolean).join(" ");
|
|
1460
|
-
const iconHtml =
|
|
1498
|
+
const iconHtml = safeIcon ? `<span class="mr-1">${safeIcon}</span>` : "";
|
|
1461
1499
|
const removeHtml = removable ? `<button
|
|
1462
1500
|
type="button"
|
|
1463
1501
|
class="ml-1.5 -mr-1 hover:opacity-70 transition-opacity"
|
|
@@ -1475,7 +1513,8 @@ function badge(text, options = {}) {
|
|
|
1475
1513
|
function badgeGroup(badges, options = {}) {
|
|
1476
1514
|
const { gap = "sm", className = "" } = options;
|
|
1477
1515
|
const gapClasses = { sm: "gap-1", md: "gap-2", lg: "gap-3" };
|
|
1478
|
-
|
|
1516
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1517
|
+
return `<div class="inline-flex flex-wrap ${gapClasses[gap]} ${safeClassName}">
|
|
1479
1518
|
${badges.join("\n")}
|
|
1480
1519
|
</div>`;
|
|
1481
1520
|
}
|
|
@@ -1491,6 +1530,7 @@ var busyDot = (label = "Busy") => badge(label, { variant: "danger", dot: true })
|
|
|
1491
1530
|
var awayDot = (label = "Away") => badge(label, { variant: "warning", dot: true });
|
|
1492
1531
|
|
|
1493
1532
|
// libs/ui/src/components/alert.ts
|
|
1533
|
+
var import_runtime4 = require("@frontmcp/uipack/runtime");
|
|
1494
1534
|
var alertIcons = {
|
|
1495
1535
|
info: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1496
1536
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
@@ -1534,11 +1574,24 @@ function getVariantClasses4(variant) {
|
|
|
1534
1574
|
return variants[variant];
|
|
1535
1575
|
}
|
|
1536
1576
|
function alert(message, options = {}) {
|
|
1537
|
-
const {
|
|
1577
|
+
const {
|
|
1578
|
+
variant = "info",
|
|
1579
|
+
title,
|
|
1580
|
+
showIcon = true,
|
|
1581
|
+
icon,
|
|
1582
|
+
dismissible = false,
|
|
1583
|
+
className = "",
|
|
1584
|
+
id,
|
|
1585
|
+
actions,
|
|
1586
|
+
sanitize = false
|
|
1587
|
+
} = options;
|
|
1588
|
+
const safeIcon = sanitize && icon ? (0, import_runtime4.sanitizeHtmlContent)(icon) : icon;
|
|
1589
|
+
const safeActions = sanitize && actions ? (0, import_runtime4.sanitizeHtmlContent)(actions) : actions;
|
|
1538
1590
|
const variantClasses = getVariantClasses4(variant);
|
|
1539
|
-
const
|
|
1591
|
+
const safeClassName = className ? (0, import_utils2.escapeHtml)(className) : "";
|
|
1592
|
+
const baseClasses = ["rounded-lg border p-4", variantClasses.container, safeClassName].filter(Boolean).join(" ");
|
|
1540
1593
|
const iconHtml = showIcon ? `<div class="flex-shrink-0 ${variantClasses.icon}">
|
|
1541
|
-
${
|
|
1594
|
+
${safeIcon || alertIcons[variant]}
|
|
1542
1595
|
</div>` : "";
|
|
1543
1596
|
const titleHtml = title ? `<h3 class="font-semibold">${(0, import_utils2.escapeHtml)(title)}</h3>` : "";
|
|
1544
1597
|
const dismissHtml = dismissible ? `<button
|
|
@@ -1551,7 +1604,7 @@ function alert(message, options = {}) {
|
|
|
1551
1604
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
1552
1605
|
</svg>
|
|
1553
1606
|
</button>` : "";
|
|
1554
|
-
const actionsHtml =
|
|
1607
|
+
const actionsHtml = safeActions ? `<div class="mt-3">${safeActions}</div>` : "";
|
|
1555
1608
|
const idAttr = id ? `id="${(0, import_utils2.escapeHtml)(id)}"` : "";
|
|
1556
1609
|
return `<div class="alert ${baseClasses}" role="alert" ${idAttr}>
|
|
1557
1610
|
<div class="flex gap-3">
|
package/esm/bridge/index.mjs
CHANGED
|
@@ -1455,6 +1455,13 @@ var ExtAppsNotSupportedError = class extends Error {
|
|
|
1455
1455
|
};
|
|
1456
1456
|
|
|
1457
1457
|
// libs/ui/src/bridge/adapters/claude.adapter.ts
|
|
1458
|
+
var CLAUDE_DOMAINS = ["claude.ai", "anthropic.com"];
|
|
1459
|
+
function isValidClaudeDomain(hostname) {
|
|
1460
|
+
const lowerHost = hostname.toLowerCase();
|
|
1461
|
+
return CLAUDE_DOMAINS.some(
|
|
1462
|
+
(domain) => lowerHost === domain || lowerHost.endsWith("." + domain)
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1458
1465
|
var ClaudeAdapter = class extends BaseAdapter {
|
|
1459
1466
|
id = "claude";
|
|
1460
1467
|
name = "Claude (Anthropic)";
|
|
@@ -1495,8 +1502,7 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
1495
1502
|
if (win.claude) return true;
|
|
1496
1503
|
if (win.__claudeArtifact) return true;
|
|
1497
1504
|
if (typeof location !== "undefined") {
|
|
1498
|
-
|
|
1499
|
-
if (href.includes("claude.ai") || href.includes("anthropic.com")) {
|
|
1505
|
+
if (isValidClaudeDomain(location.hostname)) {
|
|
1500
1506
|
return true;
|
|
1501
1507
|
}
|
|
1502
1508
|
}
|
|
@@ -1556,6 +1562,13 @@ function createClaudeAdapter() {
|
|
|
1556
1562
|
}
|
|
1557
1563
|
|
|
1558
1564
|
// libs/ui/src/bridge/adapters/gemini.adapter.ts
|
|
1565
|
+
var GEMINI_DOMAINS = ["gemini.google.com", "bard.google.com"];
|
|
1566
|
+
function isValidGeminiDomain(hostname) {
|
|
1567
|
+
const lowerHost = hostname.toLowerCase();
|
|
1568
|
+
return GEMINI_DOMAINS.some(
|
|
1569
|
+
(domain) => lowerHost === domain || lowerHost.endsWith("." + domain)
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1559
1572
|
var GeminiAdapter = class extends BaseAdapter {
|
|
1560
1573
|
id = "gemini";
|
|
1561
1574
|
name = "Google Gemini";
|
|
@@ -1585,8 +1598,7 @@ var GeminiAdapter = class extends BaseAdapter {
|
|
|
1585
1598
|
if (win.__mcpPlatform === "gemini") return true;
|
|
1586
1599
|
if (win.gemini) return true;
|
|
1587
1600
|
if (typeof location !== "undefined") {
|
|
1588
|
-
|
|
1589
|
-
if (href.includes("gemini.google.com") || href.includes("bard.google.com")) {
|
|
1601
|
+
if (isValidGeminiDomain(location.hostname)) {
|
|
1590
1602
|
return true;
|
|
1591
1603
|
}
|
|
1592
1604
|
}
|
package/esm/components/index.mjs
CHANGED
|
@@ -856,6 +856,7 @@ var dangerButton = (text, opts) => button(text, { ...opts, variant: "danger" });
|
|
|
856
856
|
var linkButton = (text, opts) => button(text, { ...opts, variant: "link" });
|
|
857
857
|
|
|
858
858
|
// libs/ui/src/components/card.ts
|
|
859
|
+
import { sanitizeHtmlContent } from "@frontmcp/uipack/runtime";
|
|
859
860
|
function getVariantClasses2(variant) {
|
|
860
861
|
const variants = {
|
|
861
862
|
default: "bg-white border border-border rounded-xl shadow-sm",
|
|
@@ -890,33 +891,38 @@ function card(content, options = {}) {
|
|
|
890
891
|
id,
|
|
891
892
|
data,
|
|
892
893
|
clickable = false,
|
|
893
|
-
href
|
|
894
|
+
href,
|
|
895
|
+
sanitize = false
|
|
894
896
|
} = options;
|
|
897
|
+
const safeContent = sanitize ? sanitizeHtmlContent(content) : content;
|
|
898
|
+
const safeHeaderActions = sanitize && headerActions ? sanitizeHtmlContent(headerActions) : headerActions;
|
|
899
|
+
const safeFooter = sanitize && footer ? sanitizeHtmlContent(footer) : footer;
|
|
895
900
|
const variantClasses = getVariantClasses2(variant);
|
|
896
901
|
const sizeClasses = getSizeClasses2(size);
|
|
897
902
|
const clickableClasses = clickable ? "cursor-pointer hover:shadow-md transition-shadow" : "";
|
|
898
|
-
const
|
|
903
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
904
|
+
const allClasses = [variantClasses, sizeClasses, clickableClasses, safeClassName].filter(Boolean).join(" ");
|
|
899
905
|
const dataAttrs = buildDataAttrs(data);
|
|
900
906
|
const idAttr = id ? `id="${escapeHtml2(id)}"` : "";
|
|
901
|
-
const hasHeader = title || subtitle ||
|
|
907
|
+
const hasHeader = title || subtitle || safeHeaderActions;
|
|
902
908
|
const headerHtml = hasHeader ? `<div class="flex items-start justify-between mb-4">
|
|
903
909
|
<div>
|
|
904
910
|
${title ? `<h3 class="text-lg font-semibold text-text-primary">${escapeHtml2(title)}</h3>` : ""}
|
|
905
911
|
${subtitle ? `<p class="text-sm text-text-secondary mt-1">${escapeHtml2(subtitle)}</p>` : ""}
|
|
906
912
|
</div>
|
|
907
|
-
${
|
|
913
|
+
${safeHeaderActions ? `<div class="flex items-center gap-2">${safeHeaderActions}</div>` : ""}
|
|
908
914
|
</div>` : "";
|
|
909
|
-
const footerHtml =
|
|
915
|
+
const footerHtml = safeFooter ? `<div class="mt-4 pt-4 border-t border-divider">${safeFooter}</div>` : "";
|
|
910
916
|
if (href) {
|
|
911
917
|
return `<a href="${escapeHtml2(href)}" class="${allClasses}" ${idAttr} ${dataAttrs}>
|
|
912
918
|
${headerHtml}
|
|
913
|
-
${
|
|
919
|
+
${safeContent}
|
|
914
920
|
${footerHtml}
|
|
915
921
|
</a>`;
|
|
916
922
|
}
|
|
917
923
|
return `<div class="${allClasses}" ${idAttr} ${dataAttrs}>
|
|
918
924
|
${headerHtml}
|
|
919
|
-
${
|
|
925
|
+
${safeContent}
|
|
920
926
|
${footerHtml}
|
|
921
927
|
</div>`;
|
|
922
928
|
}
|
|
@@ -924,12 +930,14 @@ function cardGroup(cards, options = {}) {
|
|
|
924
930
|
const { direction = "vertical", gap = "md", className = "" } = options;
|
|
925
931
|
const gapClasses = { sm: "gap-2", md: "gap-4", lg: "gap-6" };
|
|
926
932
|
const directionClasses = direction === "horizontal" ? "flex flex-row flex-wrap" : "flex flex-col";
|
|
927
|
-
|
|
933
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
934
|
+
return `<div class="${directionClasses} ${gapClasses[gap]} ${safeClassName}">
|
|
928
935
|
${cards.join("\n")}
|
|
929
936
|
</div>`;
|
|
930
937
|
}
|
|
931
938
|
|
|
932
939
|
// libs/ui/src/components/form.ts
|
|
940
|
+
import { sanitizeHtmlContent as sanitizeHtmlContent2 } from "@frontmcp/uipack/runtime";
|
|
933
941
|
function getInputSizeClasses(size) {
|
|
934
942
|
const sizes = {
|
|
935
943
|
sm: "px-3 py-1.5 text-sm",
|
|
@@ -974,11 +982,15 @@ function input(options) {
|
|
|
974
982
|
className = "",
|
|
975
983
|
data,
|
|
976
984
|
iconBefore,
|
|
977
|
-
iconAfter
|
|
985
|
+
iconAfter,
|
|
986
|
+
sanitize = false
|
|
978
987
|
} = options;
|
|
988
|
+
const safeIconBefore = sanitize && iconBefore ? sanitizeHtmlContent2(iconBefore) : iconBefore;
|
|
989
|
+
const safeIconAfter = sanitize && iconAfter ? sanitizeHtmlContent2(iconAfter) : iconAfter;
|
|
979
990
|
const sizeClasses = getInputSizeClasses(size);
|
|
980
991
|
const stateClasses = getInputStateClasses(state);
|
|
981
|
-
const hasIcon =
|
|
992
|
+
const hasIcon = safeIconBefore || safeIconAfter;
|
|
993
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
982
994
|
const baseClasses = [
|
|
983
995
|
"w-full rounded-lg border bg-white",
|
|
984
996
|
"transition-colors duration-200",
|
|
@@ -986,8 +998,8 @@ function input(options) {
|
|
|
986
998
|
disabled ? "opacity-50 cursor-not-allowed bg-gray-50" : "",
|
|
987
999
|
sizeClasses,
|
|
988
1000
|
stateClasses,
|
|
989
|
-
hasIcon ? (
|
|
990
|
-
|
|
1001
|
+
hasIcon ? (safeIconBefore ? "pl-10" : "") + (safeIconAfter ? " pr-10" : "") : "",
|
|
1002
|
+
safeClassName
|
|
991
1003
|
].filter(Boolean).join(" ");
|
|
992
1004
|
const dataAttrs = buildDataAttrs2(data);
|
|
993
1005
|
const inputAttrs = [
|
|
@@ -1012,8 +1024,8 @@ function input(options) {
|
|
|
1012
1024
|
</label>` : "";
|
|
1013
1025
|
const helperHtml = helper && !error ? `<p class="mt-1.5 text-sm text-text-secondary">${escapeHtml2(helper)}</p>` : "";
|
|
1014
1026
|
const errorHtml = error ? `<p class="mt-1.5 text-sm text-danger">${escapeHtml2(error)}</p>` : "";
|
|
1015
|
-
const iconBeforeHtml =
|
|
1016
|
-
const iconAfterHtml =
|
|
1027
|
+
const iconBeforeHtml = safeIconBefore ? `<span class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary">${safeIconBefore}</span>` : "";
|
|
1028
|
+
const iconAfterHtml = safeIconAfter ? `<span class="absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary">${safeIconAfter}</span>` : "";
|
|
1017
1029
|
const inputHtml = hasIcon ? `<div class="relative">
|
|
1018
1030
|
${iconBeforeHtml}
|
|
1019
1031
|
<input ${inputAttrs}>
|
|
@@ -1045,6 +1057,7 @@ function select(options) {
|
|
|
1045
1057
|
} = options;
|
|
1046
1058
|
const sizeClasses = getInputSizeClasses(size);
|
|
1047
1059
|
const stateClasses = getInputStateClasses(state);
|
|
1060
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
1048
1061
|
const baseClasses = [
|
|
1049
1062
|
"w-full rounded-lg border bg-white",
|
|
1050
1063
|
"transition-colors duration-200",
|
|
@@ -1052,7 +1065,7 @@ function select(options) {
|
|
|
1052
1065
|
disabled ? "opacity-50 cursor-not-allowed bg-gray-50" : "",
|
|
1053
1066
|
sizeClasses,
|
|
1054
1067
|
stateClasses,
|
|
1055
|
-
|
|
1068
|
+
safeClassName
|
|
1056
1069
|
].filter(Boolean).join(" ");
|
|
1057
1070
|
const dataAttrs = buildDataAttrs2(data);
|
|
1058
1071
|
const optionsHtml = selectOptions.map((opt) => {
|
|
@@ -1109,6 +1122,7 @@ function textarea(options) {
|
|
|
1109
1122
|
horizontal: "resize-x",
|
|
1110
1123
|
both: "resize"
|
|
1111
1124
|
};
|
|
1125
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
1112
1126
|
const baseClasses = [
|
|
1113
1127
|
"w-full rounded-lg border bg-white",
|
|
1114
1128
|
"transition-colors duration-200",
|
|
@@ -1117,7 +1131,7 @@ function textarea(options) {
|
|
|
1117
1131
|
sizeClasses,
|
|
1118
1132
|
stateClasses,
|
|
1119
1133
|
resizeClasses[resize],
|
|
1120
|
-
|
|
1134
|
+
safeClassName
|
|
1121
1135
|
].filter(Boolean).join(" ");
|
|
1122
1136
|
const dataAttrs = buildDataAttrs2(data);
|
|
1123
1137
|
const labelHtml = label ? `<label for="${escapeHtml2(id)}" class="block text-sm font-medium text-text-primary mb-1.5">
|
|
@@ -1161,7 +1175,8 @@ function checkbox(options) {
|
|
|
1161
1175
|
].join(" ");
|
|
1162
1176
|
const helperHtml = helper && !error ? `<p class="text-sm text-text-secondary">${escapeHtml2(helper)}</p>` : "";
|
|
1163
1177
|
const errorHtml = error ? `<p class="text-sm text-danger">${escapeHtml2(error)}</p>` : "";
|
|
1164
|
-
|
|
1178
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
1179
|
+
return `<div class="form-field ${safeClassName}">
|
|
1165
1180
|
<label class="flex items-start gap-3 ${disabled ? "cursor-not-allowed" : "cursor-pointer"}">
|
|
1166
1181
|
<input
|
|
1167
1182
|
type="checkbox"
|
|
@@ -1205,7 +1220,8 @@ function radioGroup(options) {
|
|
|
1205
1220
|
const labelHtml = label ? `<label class="block text-sm font-medium text-text-primary mb-2">${escapeHtml2(label)}</label>` : "";
|
|
1206
1221
|
const helperHtml = helper && !error ? `<p class="mt-1.5 text-sm text-text-secondary">${escapeHtml2(helper)}</p>` : "";
|
|
1207
1222
|
const errorHtml = error ? `<p class="mt-1.5 text-sm text-danger">${escapeHtml2(error)}</p>` : "";
|
|
1208
|
-
|
|
1223
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
1224
|
+
return `<div class="form-field ${safeClassName}" role="radiogroup">
|
|
1209
1225
|
${labelHtml}
|
|
1210
1226
|
<div class="${directionClasses}">
|
|
1211
1227
|
${radiosHtml}
|
|
@@ -1227,10 +1243,26 @@ function form(content, options = {}) {
|
|
|
1227
1243
|
].filter(Boolean).join(" ");
|
|
1228
1244
|
return `<form ${attrs}>${content}</form>`;
|
|
1229
1245
|
}
|
|
1246
|
+
var GRID_COLS_MAP = {
|
|
1247
|
+
1: "grid-cols-1",
|
|
1248
|
+
2: "grid-cols-2",
|
|
1249
|
+
3: "grid-cols-3",
|
|
1250
|
+
4: "grid-cols-4",
|
|
1251
|
+
5: "grid-cols-5",
|
|
1252
|
+
6: "grid-cols-6",
|
|
1253
|
+
7: "grid-cols-7",
|
|
1254
|
+
8: "grid-cols-8",
|
|
1255
|
+
9: "grid-cols-9",
|
|
1256
|
+
10: "grid-cols-10",
|
|
1257
|
+
11: "grid-cols-11",
|
|
1258
|
+
12: "grid-cols-12"
|
|
1259
|
+
};
|
|
1230
1260
|
function formRow(fields, options = {}) {
|
|
1231
1261
|
const { gap = "md", className = "" } = options;
|
|
1232
1262
|
const gapClasses = { sm: "gap-2", md: "gap-4", lg: "gap-6" };
|
|
1233
|
-
|
|
1263
|
+
const gridCols = GRID_COLS_MAP[fields.length] ?? "grid-cols-1";
|
|
1264
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
1265
|
+
return `<div class="grid ${gridCols} ${gapClasses[gap]} ${safeClassName}">
|
|
1234
1266
|
${fields.join("\n")}
|
|
1235
1267
|
</div>`;
|
|
1236
1268
|
}
|
|
@@ -1240,7 +1272,8 @@ function formSection(content, options = {}) {
|
|
|
1240
1272
|
<h3 class="text-lg font-semibold text-text-primary">${escapeHtml2(title)}</h3>
|
|
1241
1273
|
${description ? `<p class="text-sm text-text-secondary mt-1">${escapeHtml2(description)}</p>` : ""}
|
|
1242
1274
|
</div>` : "";
|
|
1243
|
-
|
|
1275
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
1276
|
+
return `<div class="form-section ${safeClassName}">
|
|
1244
1277
|
${headerHtml}
|
|
1245
1278
|
<div class="space-y-4">
|
|
1246
1279
|
${content}
|
|
@@ -1255,7 +1288,8 @@ function formActions(buttons, options = {}) {
|
|
|
1255
1288
|
right: "justify-end",
|
|
1256
1289
|
between: "justify-between"
|
|
1257
1290
|
};
|
|
1258
|
-
|
|
1291
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
1292
|
+
return `<div class="flex items-center gap-3 pt-4 ${alignClasses[align]} ${safeClassName}">
|
|
1259
1293
|
${buttons.join("\n")}
|
|
1260
1294
|
</div>`;
|
|
1261
1295
|
}
|
|
@@ -1267,6 +1301,7 @@ function csrfInput(token) {
|
|
|
1267
1301
|
}
|
|
1268
1302
|
|
|
1269
1303
|
// libs/ui/src/components/badge.ts
|
|
1304
|
+
import { sanitizeHtmlContent as sanitizeHtmlContent3 } from "@frontmcp/uipack/runtime";
|
|
1270
1305
|
function getVariantClasses3(variant) {
|
|
1271
1306
|
const variants = {
|
|
1272
1307
|
default: "bg-gray-100 text-gray-800",
|
|
@@ -1304,8 +1339,11 @@ function badge(text, options = {}) {
|
|
|
1304
1339
|
icon,
|
|
1305
1340
|
dot = false,
|
|
1306
1341
|
className = "",
|
|
1307
|
-
removable = false
|
|
1342
|
+
removable = false,
|
|
1343
|
+
sanitize = false
|
|
1308
1344
|
} = options;
|
|
1345
|
+
const safeIcon = sanitize && icon ? sanitizeHtmlContent3(icon) : icon;
|
|
1346
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
1309
1347
|
if (dot) {
|
|
1310
1348
|
const dotVariants = {
|
|
1311
1349
|
default: "bg-gray-400",
|
|
@@ -1317,7 +1355,7 @@ function badge(text, options = {}) {
|
|
|
1317
1355
|
info: "bg-blue-500",
|
|
1318
1356
|
outline: "border border-current"
|
|
1319
1357
|
};
|
|
1320
|
-
const dotClasses = ["inline-block rounded-full", getSizeClasses3(size, true), dotVariants[variant],
|
|
1358
|
+
const dotClasses = ["inline-block rounded-full", getSizeClasses3(size, true), dotVariants[variant], safeClassName].filter(Boolean).join(" ");
|
|
1321
1359
|
return `<span class="${dotClasses}" aria-label="${escapeHtml2(text)}" title="${escapeHtml2(text)}"></span>`;
|
|
1322
1360
|
}
|
|
1323
1361
|
const variantClasses = getVariantClasses3(variant);
|
|
@@ -1327,9 +1365,9 @@ function badge(text, options = {}) {
|
|
|
1327
1365
|
pill ? "rounded-full" : "rounded-md",
|
|
1328
1366
|
variantClasses,
|
|
1329
1367
|
sizeClasses,
|
|
1330
|
-
|
|
1368
|
+
safeClassName
|
|
1331
1369
|
].filter(Boolean).join(" ");
|
|
1332
|
-
const iconHtml =
|
|
1370
|
+
const iconHtml = safeIcon ? `<span class="mr-1">${safeIcon}</span>` : "";
|
|
1333
1371
|
const removeHtml = removable ? `<button
|
|
1334
1372
|
type="button"
|
|
1335
1373
|
class="ml-1.5 -mr-1 hover:opacity-70 transition-opacity"
|
|
@@ -1347,7 +1385,8 @@ function badge(text, options = {}) {
|
|
|
1347
1385
|
function badgeGroup(badges, options = {}) {
|
|
1348
1386
|
const { gap = "sm", className = "" } = options;
|
|
1349
1387
|
const gapClasses = { sm: "gap-1", md: "gap-2", lg: "gap-3" };
|
|
1350
|
-
|
|
1388
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
1389
|
+
return `<div class="inline-flex flex-wrap ${gapClasses[gap]} ${safeClassName}">
|
|
1351
1390
|
${badges.join("\n")}
|
|
1352
1391
|
</div>`;
|
|
1353
1392
|
}
|
|
@@ -1363,6 +1402,7 @@ var busyDot = (label = "Busy") => badge(label, { variant: "danger", dot: true })
|
|
|
1363
1402
|
var awayDot = (label = "Away") => badge(label, { variant: "warning", dot: true });
|
|
1364
1403
|
|
|
1365
1404
|
// libs/ui/src/components/alert.ts
|
|
1405
|
+
import { sanitizeHtmlContent as sanitizeHtmlContent4 } from "@frontmcp/uipack/runtime";
|
|
1366
1406
|
var alertIcons = {
|
|
1367
1407
|
info: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1368
1408
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
@@ -1406,11 +1446,24 @@ function getVariantClasses4(variant) {
|
|
|
1406
1446
|
return variants[variant];
|
|
1407
1447
|
}
|
|
1408
1448
|
function alert(message, options = {}) {
|
|
1409
|
-
const {
|
|
1449
|
+
const {
|
|
1450
|
+
variant = "info",
|
|
1451
|
+
title,
|
|
1452
|
+
showIcon = true,
|
|
1453
|
+
icon,
|
|
1454
|
+
dismissible = false,
|
|
1455
|
+
className = "",
|
|
1456
|
+
id,
|
|
1457
|
+
actions,
|
|
1458
|
+
sanitize = false
|
|
1459
|
+
} = options;
|
|
1460
|
+
const safeIcon = sanitize && icon ? sanitizeHtmlContent4(icon) : icon;
|
|
1461
|
+
const safeActions = sanitize && actions ? sanitizeHtmlContent4(actions) : actions;
|
|
1410
1462
|
const variantClasses = getVariantClasses4(variant);
|
|
1411
|
-
const
|
|
1463
|
+
const safeClassName = className ? escapeHtml2(className) : "";
|
|
1464
|
+
const baseClasses = ["rounded-lg border p-4", variantClasses.container, safeClassName].filter(Boolean).join(" ");
|
|
1412
1465
|
const iconHtml = showIcon ? `<div class="flex-shrink-0 ${variantClasses.icon}">
|
|
1413
|
-
${
|
|
1466
|
+
${safeIcon || alertIcons[variant]}
|
|
1414
1467
|
</div>` : "";
|
|
1415
1468
|
const titleHtml = title ? `<h3 class="font-semibold">${escapeHtml2(title)}</h3>` : "";
|
|
1416
1469
|
const dismissHtml = dismissible ? `<button
|
|
@@ -1423,7 +1476,7 @@ function alert(message, options = {}) {
|
|
|
1423
1476
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
1424
1477
|
</svg>
|
|
1425
1478
|
</button>` : "";
|
|
1426
|
-
const actionsHtml =
|
|
1479
|
+
const actionsHtml = safeActions ? `<div class="mt-3">${safeActions}</div>` : "";
|
|
1427
1480
|
const idAttr = id ? `id="${escapeHtml2(id)}"` : "";
|
|
1428
1481
|
return `<div class="alert ${baseClasses}" role="alert" ${idAttr}>
|
|
1429
1482
|
<div class="flex gap-3">
|