@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.
@@ -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 allClasses = [variantClasses, sizeClasses, clickableClasses, className].filter(Boolean).join(" ");
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 || headerActions;
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
- ${headerActions ? `<div class="flex items-center gap-2">${headerActions}</div>` : ""}
1041
+ ${safeHeaderActions ? `<div class="flex items-center gap-2">${safeHeaderActions}</div>` : ""}
1036
1042
  </div>` : "";
1037
- const footerHtml = footer ? `<div class="mt-4 pt-4 border-t border-divider">${footer}</div>` : "";
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
- ${content}
1047
+ ${safeContent}
1042
1048
  ${footerHtml}
1043
1049
  </a>`;
1044
1050
  }
1045
1051
  return `<div class="${allClasses}" ${idAttr} ${dataAttrs}>
1046
1052
  ${headerHtml}
1047
- ${content}
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
- return `<div class="${directionClasses} ${gapClasses[gap]} ${className}">
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 = iconBefore || iconAfter;
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 ? (iconBefore ? "pl-10" : "") + (iconAfter ? " pr-10" : "") : "",
1118
- className
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 = iconBefore ? `<span class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary">${iconBefore}</span>` : "";
1144
- const iconAfterHtml = iconAfter ? `<span class="absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary">${iconAfter}</span>` : "";
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
- className
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
- className
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
- return `<div class="form-field ${className}">
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
- return `<div class="form-field ${className}" role="radiogroup">
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
- return `<div class="grid grid-cols-${fields.length} ${gapClasses[gap]} ${className}">
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
- return `<div class="form-section ${className}">
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
- return `<div class="flex items-center gap-3 pt-4 ${alignClasses[align]} ${className}">
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], className].filter(Boolean).join(" ");
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
- className
1496
+ safeClassName
1459
1497
  ].filter(Boolean).join(" ");
1460
- const iconHtml = icon ? `<span class="mr-1">${icon}</span>` : "";
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
- return `<div class="inline-flex flex-wrap ${gapClasses[gap]} ${className}">
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 { variant = "info", title, showIcon = true, icon, dismissible = false, className = "", id, actions } = options;
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 baseClasses = ["rounded-lg border p-4", variantClasses.container, className].filter(Boolean).join(" ");
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
- ${icon || alertIcons[variant]}
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 = actions ? `<div class="mt-3">${actions}</div>` : "";
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">
@@ -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
- const href = location.href;
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
- const href = location.href;
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
  }
@@ -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 allClasses = [variantClasses, sizeClasses, clickableClasses, className].filter(Boolean).join(" ");
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 || headerActions;
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
- ${headerActions ? `<div class="flex items-center gap-2">${headerActions}</div>` : ""}
913
+ ${safeHeaderActions ? `<div class="flex items-center gap-2">${safeHeaderActions}</div>` : ""}
908
914
  </div>` : "";
909
- const footerHtml = footer ? `<div class="mt-4 pt-4 border-t border-divider">${footer}</div>` : "";
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
- ${content}
919
+ ${safeContent}
914
920
  ${footerHtml}
915
921
  </a>`;
916
922
  }
917
923
  return `<div class="${allClasses}" ${idAttr} ${dataAttrs}>
918
924
  ${headerHtml}
919
- ${content}
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
- return `<div class="${directionClasses} ${gapClasses[gap]} ${className}">
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 = iconBefore || iconAfter;
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 ? (iconBefore ? "pl-10" : "") + (iconAfter ? " pr-10" : "") : "",
990
- className
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 = iconBefore ? `<span class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary">${iconBefore}</span>` : "";
1016
- const iconAfterHtml = iconAfter ? `<span class="absolute right-3 top-1/2 -translate-y-1/2 text-text-secondary">${iconAfter}</span>` : "";
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
- className
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
- className
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
- return `<div class="form-field ${className}">
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
- return `<div class="form-field ${className}" role="radiogroup">
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
- return `<div class="grid grid-cols-${fields.length} ${gapClasses[gap]} ${className}">
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
- return `<div class="form-section ${className}">
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
- return `<div class="flex items-center gap-3 pt-4 ${alignClasses[align]} ${className}">
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], className].filter(Boolean).join(" ");
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
- className
1368
+ safeClassName
1331
1369
  ].filter(Boolean).join(" ");
1332
- const iconHtml = icon ? `<span class="mr-1">${icon}</span>` : "";
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
- return `<div class="inline-flex flex-wrap ${gapClasses[gap]} ${className}">
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 { variant = "info", title, showIcon = true, icon, dismissible = false, className = "", id, actions } = options;
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 baseClasses = ["rounded-lg border p-4", variantClasses.container, className].filter(Boolean).join(" ");
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
- ${icon || alertIcons[variant]}
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 = actions ? `<div class="mt-3">${actions}</div>` : "";
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">