@frontmcp/ui 0.8.0 → 0.9.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/esm/index.mjs CHANGED
@@ -1022,6 +1022,7 @@ var dangerButton = (text, opts) => button(text, { ...opts, variant: "danger" });
1022
1022
  var linkButton = (text, opts) => button(text, { ...opts, variant: "link" });
1023
1023
 
1024
1024
  // libs/ui/src/components/card.ts
1025
+ import { sanitizeHtmlContent } from "@frontmcp/uipack/runtime";
1025
1026
  function getVariantClasses2(variant) {
1026
1027
  const variants = {
1027
1028
  default: "bg-white border border-border rounded-xl shadow-sm",
@@ -1056,33 +1057,38 @@ function card(content, options = {}) {
1056
1057
  id,
1057
1058
  data,
1058
1059
  clickable = false,
1059
- href
1060
+ href,
1061
+ sanitize = false
1060
1062
  } = options;
1063
+ const safeContent = sanitize ? sanitizeHtmlContent(content) : content;
1064
+ const safeHeaderActions = sanitize && headerActions ? sanitizeHtmlContent(headerActions) : headerActions;
1065
+ const safeFooter = sanitize && footer ? sanitizeHtmlContent(footer) : footer;
1061
1066
  const variantClasses = getVariantClasses2(variant);
1062
1067
  const sizeClasses = getSizeClasses2(size);
1063
1068
  const clickableClasses = clickable ? "cursor-pointer hover:shadow-md transition-shadow" : "";
1064
- const allClasses = [variantClasses, sizeClasses, clickableClasses, className].filter(Boolean).join(" ");
1069
+ const safeClassName = className ? escapeHtml2(className) : "";
1070
+ const allClasses = [variantClasses, sizeClasses, clickableClasses, safeClassName].filter(Boolean).join(" ");
1065
1071
  const dataAttrs = buildDataAttrs(data);
1066
1072
  const idAttr = id ? `id="${escapeHtml2(id)}"` : "";
1067
- const hasHeader = title || subtitle || headerActions;
1073
+ const hasHeader = title || subtitle || safeHeaderActions;
1068
1074
  const headerHtml = hasHeader ? `<div class="flex items-start justify-between mb-4">
1069
1075
  <div>
1070
1076
  ${title ? `<h3 class="text-lg font-semibold text-text-primary">${escapeHtml2(title)}</h3>` : ""}
1071
1077
  ${subtitle ? `<p class="text-sm text-text-secondary mt-1">${escapeHtml2(subtitle)}</p>` : ""}
1072
1078
  </div>
1073
- ${headerActions ? `<div class="flex items-center gap-2">${headerActions}</div>` : ""}
1079
+ ${safeHeaderActions ? `<div class="flex items-center gap-2">${safeHeaderActions}</div>` : ""}
1074
1080
  </div>` : "";
1075
- const footerHtml = footer ? `<div class="mt-4 pt-4 border-t border-divider">${footer}</div>` : "";
1081
+ const footerHtml = safeFooter ? `<div class="mt-4 pt-4 border-t border-divider">${safeFooter}</div>` : "";
1076
1082
  if (href) {
1077
1083
  return `<a href="${escapeHtml2(href)}" class="${allClasses}" ${idAttr} ${dataAttrs}>
1078
1084
  ${headerHtml}
1079
- ${content}
1085
+ ${safeContent}
1080
1086
  ${footerHtml}
1081
1087
  </a>`;
1082
1088
  }
1083
1089
  return `<div class="${allClasses}" ${idAttr} ${dataAttrs}>
1084
1090
  ${headerHtml}
1085
- ${content}
1091
+ ${safeContent}
1086
1092
  ${footerHtml}
1087
1093
  </div>`;
1088
1094
  }
@@ -1090,12 +1096,14 @@ function cardGroup(cards, options = {}) {
1090
1096
  const { direction = "vertical", gap = "md", className = "" } = options;
1091
1097
  const gapClasses = { sm: "gap-2", md: "gap-4", lg: "gap-6" };
1092
1098
  const directionClasses = direction === "horizontal" ? "flex flex-row flex-wrap" : "flex flex-col";
1093
- return `<div class="${directionClasses} ${gapClasses[gap]} ${className}">
1099
+ const safeClassName = className ? escapeHtml2(className) : "";
1100
+ return `<div class="${directionClasses} ${gapClasses[gap]} ${safeClassName}">
1094
1101
  ${cards.join("\n")}
1095
1102
  </div>`;
1096
1103
  }
1097
1104
 
1098
1105
  // libs/ui/src/components/form.ts
1106
+ import { sanitizeHtmlContent as sanitizeHtmlContent2 } from "@frontmcp/uipack/runtime";
1099
1107
  function getInputSizeClasses(size) {
1100
1108
  const sizes = {
1101
1109
  sm: "px-3 py-1.5 text-sm",
@@ -1140,11 +1148,15 @@ function input(options) {
1140
1148
  className = "",
1141
1149
  data,
1142
1150
  iconBefore,
1143
- iconAfter
1151
+ iconAfter,
1152
+ sanitize = false
1144
1153
  } = options;
1154
+ const safeIconBefore = sanitize && iconBefore ? sanitizeHtmlContent2(iconBefore) : iconBefore;
1155
+ const safeIconAfter = sanitize && iconAfter ? sanitizeHtmlContent2(iconAfter) : iconAfter;
1145
1156
  const sizeClasses = getInputSizeClasses(size);
1146
1157
  const stateClasses = getInputStateClasses(state);
1147
- const hasIcon = iconBefore || iconAfter;
1158
+ const hasIcon = safeIconBefore || safeIconAfter;
1159
+ const safeClassName = className ? escapeHtml2(className) : "";
1148
1160
  const baseClasses = [
1149
1161
  "w-full rounded-lg border bg-white",
1150
1162
  "transition-colors duration-200",
@@ -1152,8 +1164,8 @@ function input(options) {
1152
1164
  disabled ? "opacity-50 cursor-not-allowed bg-gray-50" : "",
1153
1165
  sizeClasses,
1154
1166
  stateClasses,
1155
- hasIcon ? (iconBefore ? "pl-10" : "") + (iconAfter ? " pr-10" : "") : "",
1156
- className
1167
+ hasIcon ? (safeIconBefore ? "pl-10" : "") + (safeIconAfter ? " pr-10" : "") : "",
1168
+ safeClassName
1157
1169
  ].filter(Boolean).join(" ");
1158
1170
  const dataAttrs = buildDataAttrs2(data);
1159
1171
  const inputAttrs = [
@@ -1178,8 +1190,8 @@ function input(options) {
1178
1190
  </label>` : "";
1179
1191
  const helperHtml = helper && !error ? `<p class="mt-1.5 text-sm text-text-secondary">${escapeHtml2(helper)}</p>` : "";
1180
1192
  const errorHtml = error ? `<p class="mt-1.5 text-sm text-danger">${escapeHtml2(error)}</p>` : "";
1181
- const iconBeforeHtml = iconBefore ? `<span class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary">${iconBefore}</span>` : "";
1182
- const iconAfterHtml = iconAfter ? `<span class="absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary">${iconAfter}</span>` : "";
1193
+ const iconBeforeHtml = safeIconBefore ? `<span class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary">${safeIconBefore}</span>` : "";
1194
+ const iconAfterHtml = safeIconAfter ? `<span class="absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary">${safeIconAfter}</span>` : "";
1183
1195
  const inputHtml = hasIcon ? `<div class="relative">
1184
1196
  ${iconBeforeHtml}
1185
1197
  <input ${inputAttrs}>
@@ -1211,6 +1223,7 @@ function select(options) {
1211
1223
  } = options;
1212
1224
  const sizeClasses = getInputSizeClasses(size);
1213
1225
  const stateClasses = getInputStateClasses(state);
1226
+ const safeClassName = className ? escapeHtml2(className) : "";
1214
1227
  const baseClasses = [
1215
1228
  "w-full rounded-lg border bg-white",
1216
1229
  "transition-colors duration-200",
@@ -1218,7 +1231,7 @@ function select(options) {
1218
1231
  disabled ? "opacity-50 cursor-not-allowed bg-gray-50" : "",
1219
1232
  sizeClasses,
1220
1233
  stateClasses,
1221
- className
1234
+ safeClassName
1222
1235
  ].filter(Boolean).join(" ");
1223
1236
  const dataAttrs = buildDataAttrs2(data);
1224
1237
  const optionsHtml = selectOptions.map((opt) => {
@@ -1275,6 +1288,7 @@ function textarea(options) {
1275
1288
  horizontal: "resize-x",
1276
1289
  both: "resize"
1277
1290
  };
1291
+ const safeClassName = className ? escapeHtml2(className) : "";
1278
1292
  const baseClasses = [
1279
1293
  "w-full rounded-lg border bg-white",
1280
1294
  "transition-colors duration-200",
@@ -1283,7 +1297,7 @@ function textarea(options) {
1283
1297
  sizeClasses,
1284
1298
  stateClasses,
1285
1299
  resizeClasses[resize],
1286
- className
1300
+ safeClassName
1287
1301
  ].filter(Boolean).join(" ");
1288
1302
  const dataAttrs = buildDataAttrs2(data);
1289
1303
  const labelHtml = label ? `<label for="${escapeHtml2(id)}" class="block text-sm font-medium text-text-primary mb-1.5">
@@ -1327,7 +1341,8 @@ function checkbox(options) {
1327
1341
  ].join(" ");
1328
1342
  const helperHtml = helper && !error ? `<p class="text-sm text-text-secondary">${escapeHtml2(helper)}</p>` : "";
1329
1343
  const errorHtml = error ? `<p class="text-sm text-danger">${escapeHtml2(error)}</p>` : "";
1330
- return `<div class="form-field ${className}">
1344
+ const safeClassName = className ? escapeHtml2(className) : "";
1345
+ return `<div class="form-field ${safeClassName}">
1331
1346
  <label class="flex items-start gap-3 ${disabled ? "cursor-not-allowed" : "cursor-pointer"}">
1332
1347
  <input
1333
1348
  type="checkbox"
@@ -1371,7 +1386,8 @@ function radioGroup(options) {
1371
1386
  const labelHtml = label ? `<label class="block text-sm font-medium text-text-primary mb-2">${escapeHtml2(label)}</label>` : "";
1372
1387
  const helperHtml = helper && !error ? `<p class="mt-1.5 text-sm text-text-secondary">${escapeHtml2(helper)}</p>` : "";
1373
1388
  const errorHtml = error ? `<p class="mt-1.5 text-sm text-danger">${escapeHtml2(error)}</p>` : "";
1374
- return `<div class="form-field ${className}" role="radiogroup">
1389
+ const safeClassName = className ? escapeHtml2(className) : "";
1390
+ return `<div class="form-field ${safeClassName}" role="radiogroup">
1375
1391
  ${labelHtml}
1376
1392
  <div class="${directionClasses}">
1377
1393
  ${radiosHtml}
@@ -1393,10 +1409,26 @@ function form(content, options = {}) {
1393
1409
  ].filter(Boolean).join(" ");
1394
1410
  return `<form ${attrs}>${content}</form>`;
1395
1411
  }
1412
+ var GRID_COLS_MAP = {
1413
+ 1: "grid-cols-1",
1414
+ 2: "grid-cols-2",
1415
+ 3: "grid-cols-3",
1416
+ 4: "grid-cols-4",
1417
+ 5: "grid-cols-5",
1418
+ 6: "grid-cols-6",
1419
+ 7: "grid-cols-7",
1420
+ 8: "grid-cols-8",
1421
+ 9: "grid-cols-9",
1422
+ 10: "grid-cols-10",
1423
+ 11: "grid-cols-11",
1424
+ 12: "grid-cols-12"
1425
+ };
1396
1426
  function formRow(fields, options = {}) {
1397
1427
  const { gap = "md", className = "" } = options;
1398
1428
  const gapClasses = { sm: "gap-2", md: "gap-4", lg: "gap-6" };
1399
- return `<div class="grid grid-cols-${fields.length} ${gapClasses[gap]} ${className}">
1429
+ const gridCols = GRID_COLS_MAP[fields.length] ?? "grid-cols-1";
1430
+ const safeClassName = className ? escapeHtml2(className) : "";
1431
+ return `<div class="grid ${gridCols} ${gapClasses[gap]} ${safeClassName}">
1400
1432
  ${fields.join("\n")}
1401
1433
  </div>`;
1402
1434
  }
@@ -1406,7 +1438,8 @@ function formSection(content, options = {}) {
1406
1438
  <h3 class="text-lg font-semibold text-text-primary">${escapeHtml2(title)}</h3>
1407
1439
  ${description ? `<p class="text-sm text-text-secondary mt-1">${escapeHtml2(description)}</p>` : ""}
1408
1440
  </div>` : "";
1409
- return `<div class="form-section ${className}">
1441
+ const safeClassName = className ? escapeHtml2(className) : "";
1442
+ return `<div class="form-section ${safeClassName}">
1410
1443
  ${headerHtml}
1411
1444
  <div class="space-y-4">
1412
1445
  ${content}
@@ -1421,7 +1454,8 @@ function formActions(buttons, options = {}) {
1421
1454
  right: "justify-end",
1422
1455
  between: "justify-between"
1423
1456
  };
1424
- return `<div class="flex items-center gap-3 pt-4 ${alignClasses[align]} ${className}">
1457
+ const safeClassName = className ? escapeHtml2(className) : "";
1458
+ return `<div class="flex items-center gap-3 pt-4 ${alignClasses[align]} ${safeClassName}">
1425
1459
  ${buttons.join("\n")}
1426
1460
  </div>`;
1427
1461
  }
@@ -1433,6 +1467,7 @@ function csrfInput(token) {
1433
1467
  }
1434
1468
 
1435
1469
  // libs/ui/src/components/badge.ts
1470
+ import { sanitizeHtmlContent as sanitizeHtmlContent3 } from "@frontmcp/uipack/runtime";
1436
1471
  function getVariantClasses3(variant) {
1437
1472
  const variants = {
1438
1473
  default: "bg-gray-100 text-gray-800",
@@ -1470,8 +1505,11 @@ function badge(text, options = {}) {
1470
1505
  icon,
1471
1506
  dot = false,
1472
1507
  className = "",
1473
- removable = false
1508
+ removable = false,
1509
+ sanitize = false
1474
1510
  } = options;
1511
+ const safeIcon = sanitize && icon ? sanitizeHtmlContent3(icon) : icon;
1512
+ const safeClassName = className ? escapeHtml2(className) : "";
1475
1513
  if (dot) {
1476
1514
  const dotVariants = {
1477
1515
  default: "bg-gray-400",
@@ -1483,7 +1521,7 @@ function badge(text, options = {}) {
1483
1521
  info: "bg-blue-500",
1484
1522
  outline: "border border-current"
1485
1523
  };
1486
- const dotClasses = ["inline-block rounded-full", getSizeClasses3(size, true), dotVariants[variant], className].filter(Boolean).join(" ");
1524
+ const dotClasses = ["inline-block rounded-full", getSizeClasses3(size, true), dotVariants[variant], safeClassName].filter(Boolean).join(" ");
1487
1525
  return `<span class="${dotClasses}" aria-label="${escapeHtml2(text)}" title="${escapeHtml2(text)}"></span>`;
1488
1526
  }
1489
1527
  const variantClasses = getVariantClasses3(variant);
@@ -1493,9 +1531,9 @@ function badge(text, options = {}) {
1493
1531
  pill ? "rounded-full" : "rounded-md",
1494
1532
  variantClasses,
1495
1533
  sizeClasses,
1496
- className
1534
+ safeClassName
1497
1535
  ].filter(Boolean).join(" ");
1498
- const iconHtml = icon ? `<span class="mr-1">${icon}</span>` : "";
1536
+ const iconHtml = safeIcon ? `<span class="mr-1">${safeIcon}</span>` : "";
1499
1537
  const removeHtml = removable ? `<button
1500
1538
  type="button"
1501
1539
  class="ml-1.5 -mr-1 hover:opacity-70 transition-opacity"
@@ -1513,7 +1551,8 @@ function badge(text, options = {}) {
1513
1551
  function badgeGroup(badges, options = {}) {
1514
1552
  const { gap = "sm", className = "" } = options;
1515
1553
  const gapClasses = { sm: "gap-1", md: "gap-2", lg: "gap-3" };
1516
- return `<div class="inline-flex flex-wrap ${gapClasses[gap]} ${className}">
1554
+ const safeClassName = className ? escapeHtml2(className) : "";
1555
+ return `<div class="inline-flex flex-wrap ${gapClasses[gap]} ${safeClassName}">
1517
1556
  ${badges.join("\n")}
1518
1557
  </div>`;
1519
1558
  }
@@ -1529,6 +1568,7 @@ var busyDot = (label = "Busy") => badge(label, { variant: "danger", dot: true })
1529
1568
  var awayDot = (label = "Away") => badge(label, { variant: "warning", dot: true });
1530
1569
 
1531
1570
  // libs/ui/src/components/alert.ts
1571
+ import { sanitizeHtmlContent as sanitizeHtmlContent4 } from "@frontmcp/uipack/runtime";
1532
1572
  var alertIcons = {
1533
1573
  info: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1534
1574
  <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"/>
@@ -1572,11 +1612,24 @@ function getVariantClasses4(variant) {
1572
1612
  return variants[variant];
1573
1613
  }
1574
1614
  function alert(message, options = {}) {
1575
- const { variant = "info", title, showIcon = true, icon, dismissible = false, className = "", id, actions } = options;
1615
+ const {
1616
+ variant = "info",
1617
+ title,
1618
+ showIcon = true,
1619
+ icon,
1620
+ dismissible = false,
1621
+ className = "",
1622
+ id,
1623
+ actions,
1624
+ sanitize = false
1625
+ } = options;
1626
+ const safeIcon = sanitize && icon ? sanitizeHtmlContent4(icon) : icon;
1627
+ const safeActions = sanitize && actions ? sanitizeHtmlContent4(actions) : actions;
1576
1628
  const variantClasses = getVariantClasses4(variant);
1577
- const baseClasses = ["rounded-lg border p-4", variantClasses.container, className].filter(Boolean).join(" ");
1629
+ const safeClassName = className ? escapeHtml2(className) : "";
1630
+ const baseClasses = ["rounded-lg border p-4", variantClasses.container, safeClassName].filter(Boolean).join(" ");
1578
1631
  const iconHtml = showIcon ? `<div class="flex-shrink-0 ${variantClasses.icon}">
1579
- ${icon || alertIcons[variant]}
1632
+ ${safeIcon || alertIcons[variant]}
1580
1633
  </div>` : "";
1581
1634
  const titleHtml = title ? `<h3 class="font-semibold">${escapeHtml2(title)}</h3>` : "";
1582
1635
  const dismissHtml = dismissible ? `<button
@@ -1589,7 +1642,7 @@ function alert(message, options = {}) {
1589
1642
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
1590
1643
  </svg>
1591
1644
  </button>` : "";
1592
- const actionsHtml = actions ? `<div class="mt-3">${actions}</div>` : "";
1645
+ const actionsHtml = safeActions ? `<div class="mt-3">${safeActions}</div>` : "";
1593
1646
  const idAttr = id ? `id="${escapeHtml2(id)}"` : "";
1594
1647
  return `<div class="alert ${baseClasses}" role="alert" ${idAttr}>
1595
1648
  <div class="flex gap-3">
@@ -5088,7 +5141,17 @@ var ReactRendererAdapter = class {
5088
5141
  return true;
5089
5142
  }
5090
5143
  if (typeof content === "string") {
5091
- return content.includes("React.createElement") || content.includes("jsx(") || content.includes("jsxs(") || /function\s+\w+\s*\([^)]*\)\s*\{[\s\S]*return\s*[\s\S]*</.test(content);
5144
+ if (content.includes("React.createElement") || content.includes("jsx(") || content.includes("jsxs(")) {
5145
+ return true;
5146
+ }
5147
+ const funcIndex = content.indexOf("function");
5148
+ if (funcIndex !== -1) {
5149
+ const afterFunc = content.slice(funcIndex, Math.min(funcIndex + 2e3, content.length));
5150
+ if (afterFunc.includes("return") && afterFunc.includes("<")) {
5151
+ return true;
5152
+ }
5153
+ }
5154
+ return false;
5092
5155
  }
5093
5156
  return false;
5094
5157
  }
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/ui",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "FrontMCP UI - React components and hooks for MCP applications",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",
@@ -64,7 +64,7 @@
64
64
  "peerDependencies": {
65
65
  "react": "^18.0.0 || ^19.0.0",
66
66
  "react-dom": "^18.0.0 || ^19.0.0",
67
- "@frontmcp/uipack": "0.8.0"
67
+ "@frontmcp/uipack": "0.9.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/react": "^19.0.0",
@@ -206,7 +206,17 @@ var ReactRendererAdapter = class {
206
206
  return true;
207
207
  }
208
208
  if (typeof content === "string") {
209
- return content.includes("React.createElement") || content.includes("jsx(") || content.includes("jsxs(") || /function\s+\w+\s*\([^)]*\)\s*\{[\s\S]*return\s*[\s\S]*</.test(content);
209
+ if (content.includes("React.createElement") || content.includes("jsx(") || content.includes("jsxs(")) {
210
+ return true;
211
+ }
212
+ const funcIndex = content.indexOf("function");
213
+ if (funcIndex !== -1) {
214
+ const afterFunc = content.slice(funcIndex, Math.min(funcIndex + 2e3, content.length));
215
+ if (afterFunc.includes("return") && afterFunc.includes("<")) {
216
+ return true;
217
+ }
218
+ }
219
+ return false;
210
220
  }
211
221
  return false;
212
222
  }
@@ -202,6 +202,8 @@ function withFrontMCP(Component) {
202
202
 
203
203
  // libs/ui/src/universal/renderers/html.renderer.ts
204
204
  import React2 from "react";
205
+ import { sanitizeHtmlContent } from "@frontmcp/uipack/runtime";
206
+ var sanitizeHtml = sanitizeHtmlContent;
205
207
  var htmlRenderer = {
206
208
  type: "html",
207
209
  priority: 0,
@@ -220,13 +222,6 @@ var htmlRenderer = {
220
222
  });
221
223
  }
222
224
  };
223
- function sanitizeHtml(html) {
224
- let sanitized = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
225
- sanitized = sanitized.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, "");
226
- sanitized = sanitized.replace(/\s+on\w+\s*=\s*[^\s>]*/gi, "");
227
- sanitized = sanitized.replace(/href\s*=\s*["']javascript:[^"']*["']/gi, 'href="#"');
228
- return sanitized;
229
- }
230
225
  var safeHtmlRenderer = {
231
226
  type: "html",
232
227
  priority: 0,
@@ -618,6 +618,7 @@ function registerFmcpButton() {
618
618
  }
619
619
 
620
620
  // libs/ui/src/components/card.ts
621
+ import { sanitizeHtmlContent } from "@frontmcp/uipack/runtime";
621
622
  function getVariantClasses2(variant) {
622
623
  const variants = {
623
624
  default: "bg-white border border-border rounded-xl shadow-sm",
@@ -652,33 +653,38 @@ function card(content, options = {}) {
652
653
  id,
653
654
  data,
654
655
  clickable = false,
655
- href
656
+ href,
657
+ sanitize = false
656
658
  } = options;
659
+ const safeContent = sanitize ? sanitizeHtmlContent(content) : content;
660
+ const safeHeaderActions = sanitize && headerActions ? sanitizeHtmlContent(headerActions) : headerActions;
661
+ const safeFooter = sanitize && footer ? sanitizeHtmlContent(footer) : footer;
657
662
  const variantClasses = getVariantClasses2(variant);
658
663
  const sizeClasses = getSizeClasses2(size);
659
664
  const clickableClasses = clickable ? "cursor-pointer hover:shadow-md transition-shadow" : "";
660
- const allClasses = [variantClasses, sizeClasses, clickableClasses, className].filter(Boolean).join(" ");
665
+ const safeClassName = className ? escapeHtml2(className) : "";
666
+ const allClasses = [variantClasses, sizeClasses, clickableClasses, safeClassName].filter(Boolean).join(" ");
661
667
  const dataAttrs = buildDataAttrs(data);
662
668
  const idAttr = id ? `id="${escapeHtml2(id)}"` : "";
663
- const hasHeader = title || subtitle || headerActions;
669
+ const hasHeader = title || subtitle || safeHeaderActions;
664
670
  const headerHtml = hasHeader ? `<div class="flex items-start justify-between mb-4">
665
671
  <div>
666
672
  ${title ? `<h3 class="text-lg font-semibold text-text-primary">${escapeHtml2(title)}</h3>` : ""}
667
673
  ${subtitle ? `<p class="text-sm text-text-secondary mt-1">${escapeHtml2(subtitle)}</p>` : ""}
668
674
  </div>
669
- ${headerActions ? `<div class="flex items-center gap-2">${headerActions}</div>` : ""}
675
+ ${safeHeaderActions ? `<div class="flex items-center gap-2">${safeHeaderActions}</div>` : ""}
670
676
  </div>` : "";
671
- const footerHtml = footer ? `<div class="mt-4 pt-4 border-t border-divider">${footer}</div>` : "";
677
+ const footerHtml = safeFooter ? `<div class="mt-4 pt-4 border-t border-divider">${safeFooter}</div>` : "";
672
678
  if (href) {
673
679
  return `<a href="${escapeHtml2(href)}" class="${allClasses}" ${idAttr} ${dataAttrs}>
674
680
  ${headerHtml}
675
- ${content}
681
+ ${safeContent}
676
682
  ${footerHtml}
677
683
  </a>`;
678
684
  }
679
685
  return `<div class="${allClasses}" ${idAttr} ${dataAttrs}>
680
686
  ${headerHtml}
681
- ${content}
687
+ ${safeContent}
682
688
  ${footerHtml}
683
689
  </div>`;
684
690
  }
@@ -801,6 +807,7 @@ function registerFmcpCard() {
801
807
  }
802
808
 
803
809
  // libs/ui/src/components/alert.ts
810
+ import { sanitizeHtmlContent as sanitizeHtmlContent2 } from "@frontmcp/uipack/runtime";
804
811
  var alertIcons = {
805
812
  info: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
806
813
  <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"/>
@@ -844,11 +851,24 @@ function getVariantClasses3(variant) {
844
851
  return variants[variant];
845
852
  }
846
853
  function alert(message, options = {}) {
847
- const { variant = "info", title, showIcon = true, icon, dismissible = false, className = "", id, actions } = options;
854
+ const {
855
+ variant = "info",
856
+ title,
857
+ showIcon = true,
858
+ icon,
859
+ dismissible = false,
860
+ className = "",
861
+ id,
862
+ actions,
863
+ sanitize = false
864
+ } = options;
865
+ const safeIcon = sanitize && icon ? sanitizeHtmlContent2(icon) : icon;
866
+ const safeActions = sanitize && actions ? sanitizeHtmlContent2(actions) : actions;
848
867
  const variantClasses = getVariantClasses3(variant);
849
- const baseClasses = ["rounded-lg border p-4", variantClasses.container, className].filter(Boolean).join(" ");
868
+ const safeClassName = className ? escapeHtml2(className) : "";
869
+ const baseClasses = ["rounded-lg border p-4", variantClasses.container, safeClassName].filter(Boolean).join(" ");
850
870
  const iconHtml = showIcon ? `<div class="flex-shrink-0 ${variantClasses.icon}">
851
- ${icon || alertIcons[variant]}
871
+ ${safeIcon || alertIcons[variant]}
852
872
  </div>` : "";
853
873
  const titleHtml = title ? `<h3 class="font-semibold">${escapeHtml2(title)}</h3>` : "";
854
874
  const dismissHtml = dismissible ? `<button
@@ -861,7 +881,7 @@ function alert(message, options = {}) {
861
881
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
862
882
  </svg>
863
883
  </button>` : "";
864
- const actionsHtml = actions ? `<div class="mt-3">${actions}</div>` : "";
884
+ const actionsHtml = safeActions ? `<div class="mt-3">${safeActions}</div>` : "";
865
885
  const idAttr = id ? `id="${escapeHtml2(id)}"` : "";
866
886
  return `<div class="alert ${baseClasses}" role="alert" ${idAttr}>
867
887
  <div class="flex gap-3">
@@ -978,6 +998,7 @@ function registerFmcpAlert() {
978
998
  }
979
999
 
980
1000
  // libs/ui/src/components/badge.ts
1001
+ import { sanitizeHtmlContent as sanitizeHtmlContent3 } from "@frontmcp/uipack/runtime";
981
1002
  function getVariantClasses4(variant) {
982
1003
  const variants = {
983
1004
  default: "bg-gray-100 text-gray-800",
@@ -1015,8 +1036,11 @@ function badge(text, options = {}) {
1015
1036
  icon,
1016
1037
  dot = false,
1017
1038
  className = "",
1018
- removable = false
1039
+ removable = false,
1040
+ sanitize = false
1019
1041
  } = options;
1042
+ const safeIcon = sanitize && icon ? sanitizeHtmlContent3(icon) : icon;
1043
+ const safeClassName = className ? escapeHtml2(className) : "";
1020
1044
  if (dot) {
1021
1045
  const dotVariants = {
1022
1046
  default: "bg-gray-400",
@@ -1028,7 +1052,7 @@ function badge(text, options = {}) {
1028
1052
  info: "bg-blue-500",
1029
1053
  outline: "border border-current"
1030
1054
  };
1031
- const dotClasses = ["inline-block rounded-full", getSizeClasses3(size, true), dotVariants[variant], className].filter(Boolean).join(" ");
1055
+ const dotClasses = ["inline-block rounded-full", getSizeClasses3(size, true), dotVariants[variant], safeClassName].filter(Boolean).join(" ");
1032
1056
  return `<span class="${dotClasses}" aria-label="${escapeHtml2(text)}" title="${escapeHtml2(text)}"></span>`;
1033
1057
  }
1034
1058
  const variantClasses = getVariantClasses4(variant);
@@ -1038,9 +1062,9 @@ function badge(text, options = {}) {
1038
1062
  pill ? "rounded-full" : "rounded-md",
1039
1063
  variantClasses,
1040
1064
  sizeClasses,
1041
- className
1065
+ safeClassName
1042
1066
  ].filter(Boolean).join(" ");
1043
- const iconHtml = icon ? `<span class="mr-1">${icon}</span>` : "";
1067
+ const iconHtml = safeIcon ? `<span class="mr-1">${safeIcon}</span>` : "";
1044
1068
  const removeHtml = removable ? `<button
1045
1069
  type="button"
1046
1070
  class="ml-1.5 -mr-1 hover:opacity-70 transition-opacity"
@@ -1159,6 +1183,7 @@ function registerFmcpBadge() {
1159
1183
  }
1160
1184
 
1161
1185
  // libs/ui/src/components/form.ts
1186
+ import { sanitizeHtmlContent as sanitizeHtmlContent4 } from "@frontmcp/uipack/runtime";
1162
1187
  function getInputSizeClasses(size) {
1163
1188
  const sizes = {
1164
1189
  sm: "px-3 py-1.5 text-sm",
@@ -1203,11 +1228,15 @@ function input(options) {
1203
1228
  className = "",
1204
1229
  data,
1205
1230
  iconBefore,
1206
- iconAfter
1231
+ iconAfter,
1232
+ sanitize = false
1207
1233
  } = options;
1234
+ const safeIconBefore = sanitize && iconBefore ? sanitizeHtmlContent4(iconBefore) : iconBefore;
1235
+ const safeIconAfter = sanitize && iconAfter ? sanitizeHtmlContent4(iconAfter) : iconAfter;
1208
1236
  const sizeClasses = getInputSizeClasses(size);
1209
1237
  const stateClasses = getInputStateClasses(state);
1210
- const hasIcon = iconBefore || iconAfter;
1238
+ const hasIcon = safeIconBefore || safeIconAfter;
1239
+ const safeClassName = className ? escapeHtml2(className) : "";
1211
1240
  const baseClasses = [
1212
1241
  "w-full rounded-lg border bg-white",
1213
1242
  "transition-colors duration-200",
@@ -1215,8 +1244,8 @@ function input(options) {
1215
1244
  disabled ? "opacity-50 cursor-not-allowed bg-gray-50" : "",
1216
1245
  sizeClasses,
1217
1246
  stateClasses,
1218
- hasIcon ? (iconBefore ? "pl-10" : "") + (iconAfter ? " pr-10" : "") : "",
1219
- className
1247
+ hasIcon ? (safeIconBefore ? "pl-10" : "") + (safeIconAfter ? " pr-10" : "") : "",
1248
+ safeClassName
1220
1249
  ].filter(Boolean).join(" ");
1221
1250
  const dataAttrs = buildDataAttrs2(data);
1222
1251
  const inputAttrs = [
@@ -1241,8 +1270,8 @@ function input(options) {
1241
1270
  </label>` : "";
1242
1271
  const helperHtml = helper && !error ? `<p class="mt-1.5 text-sm text-text-secondary">${escapeHtml2(helper)}</p>` : "";
1243
1272
  const errorHtml = error ? `<p class="mt-1.5 text-sm text-danger">${escapeHtml2(error)}</p>` : "";
1244
- const iconBeforeHtml = iconBefore ? `<span class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary">${iconBefore}</span>` : "";
1245
- const iconAfterHtml = iconAfter ? `<span class="absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary">${iconAfter}</span>` : "";
1273
+ const iconBeforeHtml = safeIconBefore ? `<span class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary">${safeIconBefore}</span>` : "";
1274
+ const iconAfterHtml = safeIconAfter ? `<span class="absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary">${safeIconAfter}</span>` : "";
1246
1275
  const inputHtml = hasIcon ? `<div class="relative">
1247
1276
  ${iconBeforeHtml}
1248
1277
  <input ${inputAttrs}>
@@ -1274,6 +1303,7 @@ function select(options) {
1274
1303
  } = options;
1275
1304
  const sizeClasses = getInputSizeClasses(size);
1276
1305
  const stateClasses = getInputStateClasses(state);
1306
+ const safeClassName = className ? escapeHtml2(className) : "";
1277
1307
  const baseClasses = [
1278
1308
  "w-full rounded-lg border bg-white",
1279
1309
  "transition-colors duration-200",
@@ -1281,7 +1311,7 @@ function select(options) {
1281
1311
  disabled ? "opacity-50 cursor-not-allowed bg-gray-50" : "",
1282
1312
  sizeClasses,
1283
1313
  stateClasses,
1284
- className
1314
+ safeClassName
1285
1315
  ].filter(Boolean).join(" ");
1286
1316
  const dataAttrs = buildDataAttrs2(data);
1287
1317
  const optionsHtml = selectOptions.map((opt) => {