@safe-ugc-ui/react 0.6.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  // src/UGCRenderer.tsx
2
- import { useEffect, useMemo as useMemo2, useState as useState2 } from "react";
2
+ import { useEffect as useEffect2, useMemo as useMemo2, useState as useState4 } from "react";
3
3
  import { validate, validateRaw } from "@safe-ugc-ui/validator";
4
- import { COMPACT_BREAKPOINT_MAX_WIDTH } from "@safe-ugc-ui/types";
4
+ import {
5
+ COMPACT_BREAKPOINT_MAX_WIDTH,
6
+ MEDIUM_BREAKPOINT_MAX_WIDTH
7
+ } from "@safe-ugc-ui/types";
5
8
 
6
9
  // src/UGCContainer.tsx
7
10
  import { forwardRef } from "react";
@@ -29,7 +32,9 @@ import {
29
32
  } from "@safe-ugc-ui/types";
30
33
 
31
34
  // src/state-resolver.ts
32
- import { PROTOTYPE_POLLUTION_SEGMENTS } from "@safe-ugc-ui/types";
35
+ import {
36
+ PROTOTYPE_POLLUTION_SEGMENTS
37
+ } from "@safe-ugc-ui/types";
33
38
  function parseRefSegments(path) {
34
39
  const segments = [];
35
40
  const dotParts = path.split(".");
@@ -89,6 +94,111 @@ function resolveValue(value, state, locals) {
89
94
  }
90
95
  return value;
91
96
  }
97
+ function stringifyTextScalar(value) {
98
+ if (value === void 0) return "";
99
+ if (value === null) return "null";
100
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
101
+ return String(value);
102
+ }
103
+ return "";
104
+ }
105
+ function isTemplateObject(value) {
106
+ return typeof value === "object" && value !== null && "$template" in value && Array.isArray(value.$template);
107
+ }
108
+ function resolveTemplate(value, state, locals) {
109
+ if (!isTemplateObject(value)) {
110
+ return void 0;
111
+ }
112
+ return value.$template.map((part) => stringifyTextScalar(resolveValue(part, state, locals))).join("");
113
+ }
114
+ function resolveTextValue(value, state, locals) {
115
+ const template = resolveTemplate(value, state, locals);
116
+ if (template !== void 0) {
117
+ return template;
118
+ }
119
+ return stringifyTextScalar(resolveValue(value, state, locals));
120
+ }
121
+
122
+ // src/condition-resolver.ts
123
+ import { MAX_CONDITION_DEPTH, isRef } from "@safe-ugc-ui/types";
124
+ function resolveOperand(operand, state, locals) {
125
+ if (isRef(operand)) {
126
+ const resolved = resolveRef(operand.$ref, state, locals);
127
+ if (resolved === null || typeof resolved === "string" || typeof resolved === "number" || typeof resolved === "boolean") {
128
+ return resolved;
129
+ }
130
+ return void 0;
131
+ }
132
+ return operand;
133
+ }
134
+ function compareValues(op, left, right) {
135
+ if (left === void 0 || right === void 0) {
136
+ return false;
137
+ }
138
+ if (op === "eq") return left === right;
139
+ if (op === "ne") return left !== right;
140
+ if (left === null || right === null) {
141
+ return false;
142
+ }
143
+ if (typeof left !== typeof right) {
144
+ return false;
145
+ }
146
+ if (typeof left !== "number" && typeof left !== "string" || typeof right !== "number" && typeof right !== "string") {
147
+ return false;
148
+ }
149
+ switch (op) {
150
+ case "gt":
151
+ return left > right;
152
+ case "gte":
153
+ return left >= right;
154
+ case "lt":
155
+ return left < right;
156
+ case "lte":
157
+ return left <= right;
158
+ default:
159
+ return false;
160
+ }
161
+ }
162
+ function evaluateCondition(condition, state, locals, depth = 1) {
163
+ if (depth > MAX_CONDITION_DEPTH) {
164
+ return false;
165
+ }
166
+ if (typeof condition === "boolean") {
167
+ return condition;
168
+ }
169
+ if (isRef(condition)) {
170
+ return resolveRef(condition.$ref, state, locals) === true;
171
+ }
172
+ if (typeof condition !== "object" || condition === null || Array.isArray(condition)) {
173
+ return false;
174
+ }
175
+ const conditionObj = condition;
176
+ switch (conditionObj.op) {
177
+ case "not":
178
+ return !evaluateCondition(conditionObj.value, state, locals, depth + 1);
179
+ case "and":
180
+ return Array.isArray(conditionObj.values) && conditionObj.values.every(
181
+ (value) => evaluateCondition(value, state, locals, depth + 1)
182
+ );
183
+ case "or":
184
+ return Array.isArray(conditionObj.values) && conditionObj.values.some(
185
+ (value) => evaluateCondition(value, state, locals, depth + 1)
186
+ );
187
+ case "eq":
188
+ case "ne":
189
+ case "gt":
190
+ case "gte":
191
+ case "lt":
192
+ case "lte":
193
+ return compareValues(
194
+ conditionObj.op,
195
+ resolveOperand(conditionObj.left, state, locals),
196
+ resolveOperand(conditionObj.right, state, locals)
197
+ );
198
+ default:
199
+ return false;
200
+ }
201
+ }
92
202
 
93
203
  // src/style-mapper.ts
94
204
  import { ALLOWED_TRANSITION_PROPERTIES } from "@safe-ugc-ui/types";
@@ -103,6 +213,7 @@ var DIRECT_MAP_PROPS = [
103
213
  "gap",
104
214
  "width",
105
215
  "height",
216
+ "aspectRatio",
106
217
  "minWidth",
107
218
  "maxWidth",
108
219
  "minHeight",
@@ -159,6 +270,19 @@ function containsForbiddenCssFunction(value) {
159
270
  const lower = value.toLowerCase();
160
271
  return FORBIDDEN_CSS_FUNCTIONS_LOWER.some((fn) => lower.includes(fn));
161
272
  }
273
+ function isValidAspectRatio(value) {
274
+ if (typeof value === "number") {
275
+ return Number.isFinite(value) && value > 0;
276
+ }
277
+ if (typeof value !== "string" || containsForbiddenCssFunction(value)) {
278
+ return false;
279
+ }
280
+ const match = value.match(/^\s*([0-9]+(?:\.[0-9]+)?)\s*\/\s*([0-9]+(?:\.[0-9]+)?)\s*$/);
281
+ if (!match) {
282
+ return false;
283
+ }
284
+ return Number(match[1]) > 0 && Number(match[2]) > 0;
285
+ }
162
286
  function resolveStyleValue(value, state, locals) {
163
287
  return resolveValue(value, state, locals);
164
288
  }
@@ -179,6 +303,19 @@ function resolveStructuredString(value, state, locals) {
179
303
  }
180
304
  return resolved;
181
305
  }
306
+ function resolveStructuredLength(value, state, locals) {
307
+ const resolved = resolveStyleValue(value, state, locals);
308
+ if (typeof resolved === "number") {
309
+ return resolved;
310
+ }
311
+ if (typeof resolved === "string" && !containsForbiddenCssFunction(resolved)) {
312
+ return resolved;
313
+ }
314
+ return void 0;
315
+ }
316
+ function toCssLength(value) {
317
+ return typeof value === "number" ? `${value}px` : value;
318
+ }
182
319
  function transformToCss(transform) {
183
320
  const parts = [];
184
321
  if (transform.rotate !== void 0) {
@@ -347,6 +484,62 @@ function resolveBorderObject(border, state, locals) {
347
484
  if (color !== void 0) resolved.color = color;
348
485
  return resolved;
349
486
  }
487
+ function clipPathToCss(clipPath) {
488
+ switch (clipPath.type) {
489
+ case "circle":
490
+ return `circle(${toCssLength(clipPath.radius)})`;
491
+ case "ellipse":
492
+ return `ellipse(${toCssLength(clipPath.rx)} ${toCssLength(clipPath.ry)})`;
493
+ case "inset": {
494
+ const base = [
495
+ toCssLength(clipPath.top),
496
+ toCssLength(clipPath.right),
497
+ toCssLength(clipPath.bottom),
498
+ toCssLength(clipPath.left)
499
+ ].join(" ");
500
+ const round = clipPath.round;
501
+ return round !== void 0 ? `inset(${base} round ${toCssLength(round)})` : `inset(${base})`;
502
+ }
503
+ default:
504
+ return "";
505
+ }
506
+ }
507
+ function resolveClipPathObject(clipPath, state, locals) {
508
+ if (!isRecord(clipPath) || typeof clipPath.type !== "string") {
509
+ return void 0;
510
+ }
511
+ switch (clipPath.type) {
512
+ case "circle": {
513
+ const radius = resolveStructuredLength(clipPath.radius, state, locals);
514
+ return radius !== void 0 ? { type: "circle", radius } : void 0;
515
+ }
516
+ case "ellipse": {
517
+ const rx = resolveStructuredLength(clipPath.rx, state, locals);
518
+ const ry = resolveStructuredLength(clipPath.ry, state, locals);
519
+ return rx !== void 0 && ry !== void 0 ? { type: "ellipse", rx, ry } : void 0;
520
+ }
521
+ case "inset": {
522
+ const top = resolveStructuredLength(clipPath.top, state, locals);
523
+ const right = resolveStructuredLength(clipPath.right, state, locals);
524
+ const bottom = resolveStructuredLength(clipPath.bottom, state, locals);
525
+ const left = resolveStructuredLength(clipPath.left, state, locals);
526
+ const round = resolveStructuredLength(clipPath.round, state, locals);
527
+ if (top === void 0 || right === void 0 || bottom === void 0 || left === void 0) {
528
+ return void 0;
529
+ }
530
+ return {
531
+ type: "inset",
532
+ top,
533
+ right,
534
+ bottom,
535
+ left,
536
+ ...round !== void 0 ? { round } : {}
537
+ };
538
+ }
539
+ default:
540
+ return void 0;
541
+ }
542
+ }
350
543
  var FLEX_ALIGNMENT_MAP = {
351
544
  start: "flex-start",
352
545
  end: "flex-end"
@@ -369,6 +562,9 @@ function mapStyle(style, state, locals) {
369
562
  if (typeof resolved === "string" && containsForbiddenCssFunction(resolved)) {
370
563
  continue;
371
564
  }
565
+ if (prop === "aspectRatio" && !isValidAspectRatio(resolved)) {
566
+ continue;
567
+ }
372
568
  css[prop] = resolved;
373
569
  }
374
570
  }
@@ -421,6 +617,17 @@ function mapStyle(style, state, locals) {
421
617
  css.transition = transitionCss;
422
618
  }
423
619
  }
620
+ const resolvedBackdropBlur = resolveStructuredNumber(style.backdropBlur, state, locals);
621
+ if (resolvedBackdropBlur !== void 0 && Number.isFinite(resolvedBackdropBlur) && resolvedBackdropBlur >= 0) {
622
+ css.backdropFilter = `blur(${resolvedBackdropBlur}px)`;
623
+ }
624
+ const resolvedClipPath = resolveClipPathObject(style.clipPath, state, locals);
625
+ if (resolvedClipPath) {
626
+ const clipPathCss = clipPathToCss(resolvedClipPath);
627
+ if (clipPathCss) {
628
+ css.clipPath = clipPathCss;
629
+ }
630
+ }
424
631
  return css;
425
632
  }
426
633
  var CSS_PROPERTY_NAME_MAP = {
@@ -520,9 +727,40 @@ function Column({ style, hoverStyle, children }) {
520
727
 
521
728
  // src/components/Text.tsx
522
729
  import { jsx as jsx5 } from "react/jsx-runtime";
523
- function Text({ content, style, hoverStyle }) {
730
+ function getClampStyle(maxLines, truncate) {
731
+ if (!maxLines || maxLines < 1) {
732
+ return void 0;
733
+ }
734
+ if (maxLines === 1) {
735
+ return {
736
+ display: "inline-block",
737
+ maxWidth: "100%",
738
+ overflow: "hidden",
739
+ whiteSpace: "nowrap",
740
+ textOverflow: truncate ?? "ellipsis"
741
+ };
742
+ }
743
+ return {
744
+ display: "-webkit-box",
745
+ maxWidth: "100%",
746
+ overflow: "hidden",
747
+ textOverflow: truncate ?? "ellipsis",
748
+ WebkitBoxOrient: "vertical",
749
+ WebkitLineClamp: maxLines
750
+ };
751
+ }
752
+ function Text({
753
+ content,
754
+ spans,
755
+ maxLines,
756
+ truncate,
757
+ style,
758
+ hoverStyle
759
+ }) {
524
760
  const { style: resolvedStyle, onMouseEnter, onMouseLeave } = useHoverStyle(style, hoverStyle);
525
- return /* @__PURE__ */ jsx5("span", { style: resolvedStyle, onMouseEnter, onMouseLeave, children: content });
761
+ const clampStyle = getClampStyle(maxLines, truncate);
762
+ const mergedStyle = clampStyle ? { ...resolvedStyle, ...clampStyle } : resolvedStyle;
763
+ return /* @__PURE__ */ jsx5("span", { style: mergedStyle, onMouseEnter, onMouseLeave, children: spans?.map((span, index) => /* @__PURE__ */ jsx5("span", { style: span.style, children: span.text }, index)) ?? content ?? "" });
526
764
  }
527
765
 
528
766
  // src/components/Image.tsx
@@ -755,13 +993,279 @@ function Toggle({ value, onToggle, onAction, disabled, style, hoverStyle }) {
755
993
  );
756
994
  }
757
995
 
996
+ // src/components/Accordion.tsx
997
+ import { useId, useState as useState2 } from "react";
998
+ import { jsx as jsx18, jsxs } from "react/jsx-runtime";
999
+ function getInitialExpandedIds(items, defaultExpanded, allowMultiple) {
1000
+ const enabledIds = new Set(
1001
+ items.filter((item) => item.disabled !== true).map((item) => item.id)
1002
+ );
1003
+ const initial = (defaultExpanded ?? []).filter((id) => enabledIds.has(id));
1004
+ return allowMultiple ? initial : initial.slice(0, 1);
1005
+ }
1006
+ function Accordion({
1007
+ items,
1008
+ allowMultiple = false,
1009
+ defaultExpanded,
1010
+ style,
1011
+ hoverStyle
1012
+ }) {
1013
+ const { style: resolvedStyle, onMouseEnter, onMouseLeave } = useHoverStyle(style, hoverStyle);
1014
+ const [expandedIds, setExpandedIds] = useState2(
1015
+ () => getInitialExpandedIds(items, defaultExpanded, allowMultiple)
1016
+ );
1017
+ const baseId = useId();
1018
+ const expandedSet = new Set(expandedIds);
1019
+ const toggleItem = (itemId, disabled) => {
1020
+ if (disabled) {
1021
+ return;
1022
+ }
1023
+ setExpandedIds((current) => {
1024
+ const currentSet = new Set(current);
1025
+ if (allowMultiple) {
1026
+ if (currentSet.has(itemId)) {
1027
+ currentSet.delete(itemId);
1028
+ } else {
1029
+ currentSet.add(itemId);
1030
+ }
1031
+ return [...currentSet];
1032
+ }
1033
+ return currentSet.has(itemId) ? [] : [itemId];
1034
+ });
1035
+ };
1036
+ return /* @__PURE__ */ jsx18("div", { style: resolvedStyle, onMouseEnter, onMouseLeave, children: items.map((item) => {
1037
+ const expanded = expandedSet.has(item.id);
1038
+ const buttonId = `${baseId}-${item.id}-button`;
1039
+ const panelId = `${baseId}-${item.id}-panel`;
1040
+ return /* @__PURE__ */ jsxs(
1041
+ "div",
1042
+ {
1043
+ style: {
1044
+ borderTop: "1px solid rgba(148, 163, 184, 0.25)"
1045
+ },
1046
+ children: [
1047
+ /* @__PURE__ */ jsxs(
1048
+ "button",
1049
+ {
1050
+ id: buttonId,
1051
+ type: "button",
1052
+ "aria-expanded": expanded,
1053
+ "aria-controls": panelId,
1054
+ disabled: item.disabled,
1055
+ onClick: () => toggleItem(item.id, item.disabled),
1056
+ style: {
1057
+ width: "100%",
1058
+ display: "flex",
1059
+ alignItems: "center",
1060
+ justifyContent: "space-between",
1061
+ padding: "12px 0",
1062
+ background: "transparent",
1063
+ border: "none",
1064
+ color: "inherit",
1065
+ textAlign: "left",
1066
+ cursor: item.disabled ? "default" : "pointer",
1067
+ opacity: item.disabled ? 0.5 : 1
1068
+ },
1069
+ children: [
1070
+ /* @__PURE__ */ jsx18("span", { children: item.label }),
1071
+ /* @__PURE__ */ jsx18("span", { "aria-hidden": "true", children: expanded ? "-" : "+" })
1072
+ ]
1073
+ }
1074
+ ),
1075
+ expanded ? /* @__PURE__ */ jsx18(
1076
+ "div",
1077
+ {
1078
+ id: panelId,
1079
+ role: "region",
1080
+ "aria-labelledby": buttonId,
1081
+ style: { paddingBottom: 12 },
1082
+ children: item.content
1083
+ }
1084
+ ) : null
1085
+ ]
1086
+ },
1087
+ item.id
1088
+ );
1089
+ }) });
1090
+ }
1091
+
1092
+ // src/components/Tabs.tsx
1093
+ import {
1094
+ useEffect,
1095
+ useId as useId2,
1096
+ useRef,
1097
+ useState as useState3
1098
+ } from "react";
1099
+ import { jsx as jsx19, jsxs as jsxs2 } from "react/jsx-runtime";
1100
+ function getEnabledTabIds(tabs) {
1101
+ return tabs.filter((tab) => tab.disabled !== true).map((tab) => tab.id);
1102
+ }
1103
+ function getInitialSelectedTab(tabs, defaultTab) {
1104
+ const enabledIds = getEnabledTabIds(tabs);
1105
+ if (enabledIds.length === 0) {
1106
+ return void 0;
1107
+ }
1108
+ if (defaultTab && enabledIds.includes(defaultTab)) {
1109
+ return defaultTab;
1110
+ }
1111
+ return enabledIds[0];
1112
+ }
1113
+ function getNextEnabledIndex(tabs, startIndex, direction) {
1114
+ for (let offset = 1; offset <= tabs.length; offset++) {
1115
+ const nextIndex = (startIndex + direction * offset + tabs.length) % tabs.length;
1116
+ if (tabs[nextIndex]?.disabled !== true) {
1117
+ return nextIndex;
1118
+ }
1119
+ }
1120
+ return startIndex;
1121
+ }
1122
+ function focusTab(buttonRefs, index) {
1123
+ buttonRefs.current[index]?.focus();
1124
+ }
1125
+ function Tabs({
1126
+ tabs,
1127
+ defaultTab,
1128
+ style,
1129
+ hoverStyle
1130
+ }) {
1131
+ const { style: resolvedStyle, onMouseEnter, onMouseLeave } = useHoverStyle(style, hoverStyle);
1132
+ const [selectedTab, setSelectedTab] = useState3(
1133
+ () => getInitialSelectedTab(tabs, defaultTab)
1134
+ );
1135
+ const baseId = useId2();
1136
+ const buttonRefs = useRef([]);
1137
+ useEffect(() => {
1138
+ setSelectedTab((current) => {
1139
+ const enabledIds = getEnabledTabIds(tabs);
1140
+ if (enabledIds.length === 0) {
1141
+ return void 0;
1142
+ }
1143
+ if (current && enabledIds.includes(current)) {
1144
+ return current;
1145
+ }
1146
+ if (defaultTab && enabledIds.includes(defaultTab)) {
1147
+ return defaultTab;
1148
+ }
1149
+ return enabledIds[0];
1150
+ });
1151
+ }, [tabs, defaultTab]);
1152
+ const selectedItem = tabs.find((tab) => tab.id === selectedTab && tab.disabled !== true) ?? tabs.find((tab) => tab.disabled !== true);
1153
+ const activateTab = (index, focus = false) => {
1154
+ const tab = tabs[index];
1155
+ if (!tab || tab.disabled) {
1156
+ return;
1157
+ }
1158
+ setSelectedTab(tab.id);
1159
+ if (focus) {
1160
+ focusTab(buttonRefs, index);
1161
+ }
1162
+ };
1163
+ const onTabKeyDown = (event, index) => {
1164
+ switch (event.key) {
1165
+ case "ArrowRight":
1166
+ case "ArrowDown": {
1167
+ event.preventDefault();
1168
+ activateTab(getNextEnabledIndex(tabs, index, 1), true);
1169
+ break;
1170
+ }
1171
+ case "ArrowLeft":
1172
+ case "ArrowUp": {
1173
+ event.preventDefault();
1174
+ activateTab(getNextEnabledIndex(tabs, index, -1), true);
1175
+ break;
1176
+ }
1177
+ case "Home": {
1178
+ event.preventDefault();
1179
+ const firstEnabledIndex = tabs.findIndex((tab) => tab.disabled !== true);
1180
+ if (firstEnabledIndex >= 0) {
1181
+ activateTab(firstEnabledIndex, true);
1182
+ }
1183
+ break;
1184
+ }
1185
+ case "End": {
1186
+ event.preventDefault();
1187
+ const lastEnabledIndex = [...tabs].map((tab, currentIndex) => ({ tab, currentIndex })).reverse().find(({ tab }) => tab.disabled !== true);
1188
+ if (lastEnabledIndex) {
1189
+ activateTab(lastEnabledIndex.currentIndex, true);
1190
+ }
1191
+ break;
1192
+ }
1193
+ default:
1194
+ break;
1195
+ }
1196
+ };
1197
+ return /* @__PURE__ */ jsxs2("div", { style: resolvedStyle, onMouseEnter, onMouseLeave, children: [
1198
+ /* @__PURE__ */ jsx19(
1199
+ "div",
1200
+ {
1201
+ role: "tablist",
1202
+ "aria-orientation": "horizontal",
1203
+ style: {
1204
+ display: "flex",
1205
+ gap: 8,
1206
+ borderBottom: "1px solid rgba(148, 163, 184, 0.25)",
1207
+ paddingBottom: 8
1208
+ },
1209
+ children: tabs.map((tab, index) => {
1210
+ const selected = selectedItem?.id === tab.id;
1211
+ const tabId = `${baseId}-${tab.id}-tab`;
1212
+ const panelId = `${baseId}-${tab.id}-panel`;
1213
+ return /* @__PURE__ */ jsx19(
1214
+ "button",
1215
+ {
1216
+ ref: (element) => {
1217
+ buttonRefs.current[index] = element;
1218
+ },
1219
+ id: tabId,
1220
+ type: "button",
1221
+ role: "tab",
1222
+ "aria-selected": selected,
1223
+ "aria-controls": panelId,
1224
+ disabled: tab.disabled,
1225
+ tabIndex: selected ? 0 : -1,
1226
+ onClick: () => activateTab(index),
1227
+ onKeyDown: (event) => onTabKeyDown(event, index),
1228
+ style: {
1229
+ padding: "8px 12px",
1230
+ borderRadius: 8,
1231
+ border: "none",
1232
+ background: selected ? "rgba(148, 163, 184, 0.16)" : "transparent",
1233
+ color: "inherit",
1234
+ cursor: tab.disabled ? "default" : "pointer",
1235
+ opacity: tab.disabled ? 0.5 : 1,
1236
+ fontWeight: selected ? 600 : 400
1237
+ },
1238
+ children: tab.label
1239
+ },
1240
+ tab.id
1241
+ );
1242
+ })
1243
+ }
1244
+ ),
1245
+ selectedItem ? /* @__PURE__ */ jsx19(
1246
+ "div",
1247
+ {
1248
+ id: `${baseId}-${selectedItem.id}-panel`,
1249
+ role: "tabpanel",
1250
+ "aria-labelledby": `${baseId}-${selectedItem.id}-tab`,
1251
+ style: { paddingTop: 12 },
1252
+ children: selectedItem.content
1253
+ }
1254
+ ) : null
1255
+ ] });
1256
+ }
1257
+
758
1258
  // src/node-renderer.tsx
759
- import { jsx as jsx18 } from "react/jsx-runtime";
1259
+ import { jsx as jsx20 } from "react/jsx-runtime";
760
1260
  function isForLoop(obj) {
761
1261
  if (obj == null || typeof obj !== "object" || Array.isArray(obj)) return false;
762
1262
  const o = obj;
763
1263
  return typeof o.for === "string" && typeof o.in === "string" && o.template != null;
764
1264
  }
1265
+ function isFragmentUse(obj) {
1266
+ if (obj == null || typeof obj !== "object" || Array.isArray(obj)) return false;
1267
+ return typeof obj.$use === "string";
1268
+ }
765
1269
  function utf8ByteLength(str) {
766
1270
  let bytes = 0;
767
1271
  for (let i = 0; i < str.length; i++) {
@@ -814,50 +1318,159 @@ function mergeStyleWithCardStyles(nodeStyle, cardStyles) {
814
1318
  hoverStyle: mergedHoverStyle
815
1319
  };
816
1320
  }
817
- function getCompactResponsiveStyle(nodeResponsive) {
1321
+ function getResponsiveOverrideStyle(nodeResponsive, mode) {
818
1322
  if (!nodeResponsive) return void 0;
819
- const compact = nodeResponsive.compact;
820
- if (compact == null || typeof compact !== "object" || Array.isArray(compact)) {
1323
+ const override = nodeResponsive[mode];
1324
+ if (override == null || typeof override !== "object" || Array.isArray(override)) {
821
1325
  return void 0;
822
1326
  }
823
- return compact;
1327
+ return override;
824
1328
  }
825
1329
  function mergeEffectiveNodeStyle(node, ctx) {
826
1330
  const baseStyle = mergeStyleWithCardStyles(node.style, ctx.cardStyles);
827
- if (!ctx.responsive.compact) {
828
- return baseStyle;
829
- }
1331
+ const mediumOverride = mergeNamedStyle(
1332
+ getResponsiveOverrideStyle(node.responsive, "medium"),
1333
+ ctx.cardStyles
1334
+ );
830
1335
  const compactOverride = mergeNamedStyle(
831
- getCompactResponsiveStyle(node.responsive),
1336
+ getResponsiveOverrideStyle(node.responsive, "compact"),
832
1337
  ctx.cardStyles
833
1338
  );
834
- if (!compactOverride) {
835
- return baseStyle;
836
- }
837
1339
  const {
838
- hoverStyle: _hoverStyle,
839
- transition: _transition,
1340
+ hoverStyle: _mediumHoverStyle,
1341
+ transition: _mediumTransition,
1342
+ ...mediumStyleWithoutInteractiveFields
1343
+ } = mediumOverride ?? {};
1344
+ const {
1345
+ hoverStyle: _compactHoverStyle,
1346
+ transition: _compactTransition,
840
1347
  ...compactStyleWithoutInteractiveFields
841
- } = compactOverride;
1348
+ } = compactOverride ?? {};
842
1349
  return {
843
1350
  ...baseStyle ?? {},
844
- ...compactStyleWithoutInteractiveFields
1351
+ ...ctx.responsive.medium ? mediumStyleWithoutInteractiveFields : {},
1352
+ ...ctx.responsive.compact ? compactStyleWithoutInteractiveFields : {}
845
1353
  };
846
1354
  }
1355
+ function resolveTextPayload(node, ctx) {
1356
+ const rawSpans = Array.isArray(node.spans) ? node.spans : void 0;
1357
+ if (rawSpans) {
1358
+ const spans = rawSpans.map((span) => {
1359
+ const rawSpan = span;
1360
+ const spanStyle = rawSpan.style != null && typeof rawSpan.style === "object" && !Array.isArray(rawSpan.style) ? rawSpan.style : void 0;
1361
+ return {
1362
+ text: resolveTextValue(rawSpan.text, ctx.state, ctx.locals),
1363
+ style: spanStyle ? mapStyle(spanStyle, ctx.state, ctx.locals) : void 0,
1364
+ rawStyleBytes: spanStyle ? utf8ByteLength(JSON.stringify(spanStyle)) : 0
1365
+ };
1366
+ });
1367
+ const combinedText = spans.map((span) => span.text).join("");
1368
+ return {
1369
+ spans: spans.map(({ rawStyleBytes: _rawStyleBytes, ...span }) => span),
1370
+ textBytes: utf8ByteLength(combinedText),
1371
+ styleBytes: spans.reduce((sum, span) => sum + span.rawStyleBytes, 0)
1372
+ };
1373
+ }
1374
+ const content = resolveTextValue(node.content, ctx.state, ctx.locals);
1375
+ return {
1376
+ content,
1377
+ textBytes: utf8ByteLength(content),
1378
+ styleBytes: 0
1379
+ };
1380
+ }
1381
+ function resolveAccordionItems(node, ctx, key) {
1382
+ const rawItems = Array.isArray(node.items) ? node.items : [];
1383
+ return rawItems.flatMap((item, index) => {
1384
+ if (item == null || typeof item !== "object" || Array.isArray(item)) {
1385
+ return [];
1386
+ }
1387
+ const rawItem = item;
1388
+ const id = typeof rawItem.id === "string" ? rawItem.id : `item-${index}`;
1389
+ const label = resolveTextValue(rawItem.label, ctx.state, ctx.locals);
1390
+ const resolvedDisabled = resolveValue(rawItem.disabled, ctx.state, ctx.locals);
1391
+ const disabled = typeof resolvedDisabled === "boolean" ? resolvedDisabled : void 0;
1392
+ const content = renderNode(
1393
+ rawItem.content,
1394
+ ctx,
1395
+ `${String(key)}.items[${index}].content`
1396
+ );
1397
+ return [{ id, label, content, disabled }];
1398
+ });
1399
+ }
1400
+ function resolveTabsItems(node, ctx, key) {
1401
+ const rawTabs = Array.isArray(node.tabs) ? node.tabs : [];
1402
+ return rawTabs.flatMap((tab, index) => {
1403
+ if (tab == null || typeof tab !== "object" || Array.isArray(tab)) {
1404
+ return [];
1405
+ }
1406
+ const rawTab = tab;
1407
+ const id = typeof rawTab.id === "string" ? rawTab.id : `tab-${index}`;
1408
+ const label = resolveTextValue(rawTab.label, ctx.state, ctx.locals);
1409
+ const resolvedDisabled = resolveValue(rawTab.disabled, ctx.state, ctx.locals);
1410
+ const disabled = typeof resolvedDisabled === "boolean" ? resolvedDisabled : void 0;
1411
+ const content = renderNode(
1412
+ rawTab.content,
1413
+ ctx,
1414
+ `${String(key)}.tabs[${index}].content`
1415
+ );
1416
+ return [{ id, label, content, disabled }];
1417
+ });
1418
+ }
847
1419
  function renderNode(node, ctx, key) {
848
1420
  if (node == null || typeof node !== "object") return null;
1421
+ if (isFragmentUse(node)) {
1422
+ if ("$if" in node && !evaluateCondition(node.$if, ctx.state, ctx.locals)) {
1423
+ return null;
1424
+ }
1425
+ if ((ctx.fragmentStack?.length ?? 0) > 0) {
1426
+ ctx.onError?.([{
1427
+ code: "RUNTIME_FRAGMENT_NESTED_USE",
1428
+ message: 'Fragments may not contain nested "$use" references',
1429
+ path: String(key)
1430
+ }]);
1431
+ return null;
1432
+ }
1433
+ if (ctx.fragmentStack?.includes(node.$use)) {
1434
+ ctx.onError?.([{
1435
+ code: "RUNTIME_FRAGMENT_CYCLE",
1436
+ message: `Fragment "${node.$use}" recursively references itself`,
1437
+ path: String(key)
1438
+ }]);
1439
+ return null;
1440
+ }
1441
+ const fragment = ctx.fragments?.[node.$use];
1442
+ if (fragment == null) {
1443
+ ctx.onError?.([{
1444
+ code: "RUNTIME_FRAGMENT_NOT_FOUND",
1445
+ message: `Fragment "${node.$use}" was not found`,
1446
+ path: String(key)
1447
+ }]);
1448
+ return null;
1449
+ }
1450
+ return renderNode(
1451
+ fragment,
1452
+ {
1453
+ ...ctx,
1454
+ fragmentStack: [...ctx.fragmentStack ?? [], node.$use]
1455
+ },
1456
+ key
1457
+ );
1458
+ }
849
1459
  const n = node;
850
1460
  if (!n.type) return null;
1461
+ if ("$if" in n && !evaluateCondition(n.$if, ctx.state, ctx.locals)) {
1462
+ return null;
1463
+ }
851
1464
  const mergedRawStyle = mergeEffectiveNodeStyle(n, ctx);
852
1465
  const rv = (val) => resolveValue(val, ctx.state, ctx.locals);
853
- const styleDelta = mergedRawStyle ? utf8ByteLength(JSON.stringify(mergedRawStyle)) : 0;
1466
+ let styleDelta = mergedRawStyle ? utf8ByteLength(JSON.stringify(mergedRawStyle)) : 0;
854
1467
  const overflowDelta = mergedRawStyle?.overflow === "auto" ? 1 : 0;
855
- let resolvedTextContent;
1468
+ let resolvedTextPayload;
856
1469
  let textDelta = 0;
857
1470
  if (n.type === "Text") {
858
- const raw = rv(n.content);
859
- resolvedTextContent = typeof raw === "string" ? raw : "";
860
- textDelta = utf8ByteLength(resolvedTextContent);
1471
+ resolvedTextPayload = resolveTextPayload(n, ctx);
1472
+ textDelta = resolvedTextPayload.textBytes;
1473
+ styleDelta += resolvedTextPayload.styleBytes;
861
1474
  }
862
1475
  if (ctx.limits.nodeCount + 1 > MAX_NODE_COUNT) {
863
1476
  ctx.onError?.([{ code: "RUNTIME_NODE_LIMIT", message: `Node count exceeds maximum of ${MAX_NODE_COUNT}`, path: String(key) }]);
@@ -885,17 +1498,31 @@ function renderNode(node, ctx, key) {
885
1498
  const childElements = renderChildren(n.children, ctx);
886
1499
  switch (n.type) {
887
1500
  case "Box":
888
- return /* @__PURE__ */ jsx18(Box, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1501
+ return /* @__PURE__ */ jsx20(Box, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
889
1502
  case "Row":
890
- return /* @__PURE__ */ jsx18(Row, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1503
+ return /* @__PURE__ */ jsx20(Row, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
891
1504
  case "Column":
892
- return /* @__PURE__ */ jsx18(Column, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1505
+ return /* @__PURE__ */ jsx20(Column, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
893
1506
  case "Stack":
894
- return /* @__PURE__ */ jsx18(Stack, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1507
+ return /* @__PURE__ */ jsx20(Stack, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
895
1508
  case "Grid":
896
- return /* @__PURE__ */ jsx18(Grid, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
897
- case "Text":
898
- return /* @__PURE__ */ jsx18(Text, { content: resolvedTextContent, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1509
+ return /* @__PURE__ */ jsx20(Grid, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1510
+ case "Text": {
1511
+ const maxLines = typeof n.maxLines === "number" ? n.maxLines : void 0;
1512
+ const truncate = n.truncate === "clip" || n.truncate === "ellipsis" ? n.truncate : void 0;
1513
+ return /* @__PURE__ */ jsx20(
1514
+ Text,
1515
+ {
1516
+ content: resolvedTextPayload?.content,
1517
+ spans: resolvedTextPayload?.spans,
1518
+ maxLines,
1519
+ truncate,
1520
+ style: cssStyle,
1521
+ hoverStyle: cssHoverStyle
1522
+ },
1523
+ key
1524
+ );
1525
+ }
899
1526
  case "Image": {
900
1527
  let src = rv(n.src);
901
1528
  if (typeof src !== "string" || !src) return null;
@@ -906,7 +1533,7 @@ function renderNode(node, ctx, key) {
906
1533
  if (typeof resolved === "string" && resolved.trim().toLowerCase().startsWith("javascript:")) return null;
907
1534
  const resolvedAlt = rv(n.alt);
908
1535
  const alt = typeof resolvedAlt === "string" ? resolvedAlt : void 0;
909
- return /* @__PURE__ */ jsx18(Image, { src: resolved, alt, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1536
+ return /* @__PURE__ */ jsx20(Image, { src: resolved, alt, style: cssStyle, hoverStyle: cssHoverStyle }, key);
910
1537
  }
911
1538
  case "Avatar": {
912
1539
  let src = rv(n.src);
@@ -918,7 +1545,7 @@ function renderNode(node, ctx, key) {
918
1545
  if (typeof resolved === "string" && resolved.trim().toLowerCase().startsWith("javascript:")) return null;
919
1546
  const resolvedSize = rv(n.size);
920
1547
  const size = typeof resolvedSize === "number" || typeof resolvedSize === "string" ? resolvedSize : void 0;
921
- return /* @__PURE__ */ jsx18(Avatar, { src: resolved, size, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1548
+ return /* @__PURE__ */ jsx20(Avatar, { src: resolved, size, style: cssStyle, hoverStyle: cssHoverStyle }, key);
922
1549
  }
923
1550
  case "Icon": {
924
1551
  const resolvedName = rv(n.name);
@@ -927,7 +1554,7 @@ function renderNode(node, ctx, key) {
927
1554
  const size = typeof resolvedSize === "number" || typeof resolvedSize === "string" ? resolvedSize : void 0;
928
1555
  const resolvedColor = rv(n.color);
929
1556
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
930
- return /* @__PURE__ */ jsx18(
1557
+ return /* @__PURE__ */ jsx20(
931
1558
  Icon,
932
1559
  {
933
1560
  name,
@@ -943,14 +1570,14 @@ function renderNode(node, ctx, key) {
943
1570
  case "Spacer": {
944
1571
  const resolvedSize = rv(n.size);
945
1572
  const size = typeof resolvedSize === "number" || typeof resolvedSize === "string" ? resolvedSize : void 0;
946
- return /* @__PURE__ */ jsx18(Spacer, { size, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1573
+ return /* @__PURE__ */ jsx20(Spacer, { size, style: cssStyle, hoverStyle: cssHoverStyle }, key);
947
1574
  }
948
1575
  case "Divider": {
949
1576
  const resolvedColor = rv(n.color);
950
1577
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
951
1578
  const resolvedThickness = rv(n.thickness);
952
1579
  const thickness = typeof resolvedThickness === "number" || typeof resolvedThickness === "string" ? resolvedThickness : void 0;
953
- return /* @__PURE__ */ jsx18(Divider, { color, thickness, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1580
+ return /* @__PURE__ */ jsx20(Divider, { color, thickness, style: cssStyle, hoverStyle: cssHoverStyle }, key);
954
1581
  }
955
1582
  case "ProgressBar": {
956
1583
  const resolvedValue = rv(n.value);
@@ -959,33 +1586,33 @@ function renderNode(node, ctx, key) {
959
1586
  const max = typeof resolvedMax === "number" ? resolvedMax : 100;
960
1587
  const resolvedColor = rv(n.color);
961
1588
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
962
- return /* @__PURE__ */ jsx18(ProgressBar, { value, max, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1589
+ return /* @__PURE__ */ jsx20(ProgressBar, { value, max, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
963
1590
  }
964
1591
  case "Badge": {
965
- const resolvedLabel = rv(n.label);
966
- const label = typeof resolvedLabel === "string" ? resolvedLabel : "";
1592
+ const label = resolveTextValue(n.label, ctx.state, ctx.locals);
967
1593
  const resolvedColor = rv(n.color);
968
1594
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
969
- return /* @__PURE__ */ jsx18(Badge, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1595
+ return /* @__PURE__ */ jsx20(Badge, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
970
1596
  }
971
1597
  case "Chip": {
972
- const resolvedLabel = rv(n.label);
973
- const label = typeof resolvedLabel === "string" ? resolvedLabel : "";
1598
+ const label = resolveTextValue(n.label, ctx.state, ctx.locals);
974
1599
  const resolvedColor = rv(n.color);
975
1600
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
976
- return /* @__PURE__ */ jsx18(Chip, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1601
+ return /* @__PURE__ */ jsx20(Chip, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
977
1602
  }
978
1603
  case "Button": {
979
- const resolvedLabel = rv(n.label);
980
- const label = typeof resolvedLabel === "string" ? resolvedLabel : "";
1604
+ const label = resolveTextValue(n.label, ctx.state, ctx.locals);
981
1605
  const actionVal = n.action;
982
1606
  const action = typeof actionVal === "string" ? actionVal : "";
983
- return /* @__PURE__ */ jsx18(
1607
+ const resolvedDisabled = rv(n.disabled);
1608
+ const disabled = typeof resolvedDisabled === "boolean" ? resolvedDisabled : void 0;
1609
+ return /* @__PURE__ */ jsx20(
984
1610
  Button,
985
1611
  {
986
1612
  label,
987
1613
  action,
988
1614
  onAction: ctx.onAction,
1615
+ disabled,
989
1616
  style: cssStyle,
990
1617
  hoverStyle: cssHoverStyle
991
1618
  },
@@ -997,12 +1624,47 @@ function renderNode(node, ctx, key) {
997
1624
  const value = typeof resolvedValue === "boolean" ? resolvedValue : false;
998
1625
  const onToggleVal = n.onToggle;
999
1626
  const onToggle = typeof onToggleVal === "string" ? onToggleVal : "";
1000
- return /* @__PURE__ */ jsx18(
1627
+ const resolvedDisabled = rv(n.disabled);
1628
+ const disabled = typeof resolvedDisabled === "boolean" ? resolvedDisabled : void 0;
1629
+ return /* @__PURE__ */ jsx20(
1001
1630
  Toggle,
1002
1631
  {
1003
1632
  value,
1004
1633
  onToggle,
1005
1634
  onAction: ctx.onAction,
1635
+ disabled,
1636
+ style: cssStyle,
1637
+ hoverStyle: cssHoverStyle
1638
+ },
1639
+ key
1640
+ );
1641
+ }
1642
+ case "Accordion": {
1643
+ const items = resolveAccordionItems(n, ctx, key);
1644
+ const allowMultiple = n.allowMultiple === true;
1645
+ const defaultExpanded = Array.isArray(n.defaultExpanded) ? n.defaultExpanded.filter(
1646
+ (value) => typeof value === "string"
1647
+ ) : void 0;
1648
+ return /* @__PURE__ */ jsx20(
1649
+ Accordion,
1650
+ {
1651
+ items,
1652
+ allowMultiple,
1653
+ defaultExpanded,
1654
+ style: cssStyle,
1655
+ hoverStyle: cssHoverStyle
1656
+ },
1657
+ key
1658
+ );
1659
+ }
1660
+ case "Tabs": {
1661
+ const tabs = resolveTabsItems(n, ctx, key);
1662
+ const defaultTab = typeof n.defaultTab === "string" ? n.defaultTab : void 0;
1663
+ return /* @__PURE__ */ jsx20(
1664
+ Tabs,
1665
+ {
1666
+ tabs,
1667
+ defaultTab,
1006
1668
  style: cssStyle,
1007
1669
  hoverStyle: cssHoverStyle
1008
1670
  },
@@ -1050,7 +1712,7 @@ function renderForLoop(loop, ctx) {
1050
1712
  }
1051
1713
  return elements;
1052
1714
  }
1053
- function renderTree(rootNode, state, assets, cardStyles, iconResolver, onAction, onError, responsive = { compact: false }) {
1715
+ function renderTree(rootNode, state, assets, cardStyles, iconResolver, onAction, onError, responsive = { compact: false, medium: false }, fragments) {
1054
1716
  const limits = {
1055
1717
  nodeCount: 0,
1056
1718
  textBytes: 0,
@@ -1061,17 +1723,22 @@ function renderTree(rootNode, state, assets, cardStyles, iconResolver, onAction,
1061
1723
  state,
1062
1724
  assets,
1063
1725
  cardStyles,
1726
+ fragments,
1727
+ fragmentStack: [],
1064
1728
  iconResolver,
1065
1729
  onAction,
1066
1730
  onError,
1067
1731
  limits,
1068
- responsive
1732
+ responsive: {
1733
+ compact: responsive.compact,
1734
+ medium: responsive.medium ?? responsive.compact
1735
+ }
1069
1736
  };
1070
1737
  return renderNode(rootNode, ctx, "root");
1071
1738
  }
1072
1739
 
1073
1740
  // src/UGCRenderer.tsx
1074
- import { jsx as jsx19 } from "react/jsx-runtime";
1741
+ import { jsx as jsx21 } from "react/jsx-runtime";
1075
1742
  function UGCRenderer({
1076
1743
  card,
1077
1744
  viewName,
@@ -1082,8 +1749,8 @@ function UGCRenderer({
1082
1749
  iconResolver,
1083
1750
  onAction
1084
1751
  }) {
1085
- const [containerElement, setContainerElement] = useState2(null);
1086
- const [containerWidth, setContainerWidth] = useState2(
1752
+ const [containerElement, setContainerElement] = useState4(null);
1753
+ const [containerWidth, setContainerWidth] = useState4(
1087
1754
  typeof window === "undefined" ? null : window.innerWidth
1088
1755
  );
1089
1756
  const result = useMemo2(() => {
@@ -1103,14 +1770,16 @@ function UGCRenderer({
1103
1770
  ...stateOverride ?? {}
1104
1771
  };
1105
1772
  const cardStyles = cardObj.styles;
1773
+ const fragments = cardObj.fragments;
1106
1774
  return {
1107
1775
  valid: true,
1108
1776
  rootNode: views[selectedView],
1109
1777
  state: mergedState,
1110
- cardStyles
1778
+ cardStyles,
1779
+ fragments
1111
1780
  };
1112
1781
  }, [card, viewName, stateOverride]);
1113
- useEffect(() => {
1782
+ useEffect2(() => {
1114
1783
  if (!containerElement) {
1115
1784
  return;
1116
1785
  }
@@ -1136,6 +1805,7 @@ function UGCRenderer({
1136
1805
  }, [containerElement]);
1137
1806
  const responsive = useMemo2(
1138
1807
  () => ({
1808
+ medium: containerWidth != null && containerWidth <= MEDIUM_BREAKPOINT_MAX_WIDTH,
1139
1809
  compact: containerWidth != null && containerWidth <= COMPACT_BREAKPOINT_MAX_WIDTH
1140
1810
  }),
1141
1811
  [containerWidth]
@@ -1146,7 +1816,7 @@ function UGCRenderer({
1146
1816
  }
1147
1817
  return null;
1148
1818
  }
1149
- return /* @__PURE__ */ jsx19(UGCContainer, { ref: setContainerElement, style: containerStyle2, children: renderTree(
1819
+ return /* @__PURE__ */ jsx21(UGCContainer, { ref: setContainerElement, style: containerStyle2, children: renderTree(
1150
1820
  result.rootNode,
1151
1821
  result.state,
1152
1822
  assets,
@@ -1154,10 +1824,12 @@ function UGCRenderer({
1154
1824
  iconResolver,
1155
1825
  onAction,
1156
1826
  onError,
1157
- responsive
1827
+ responsive,
1828
+ result.fragments
1158
1829
  ) });
1159
1830
  }
1160
1831
  export {
1832
+ Accordion,
1161
1833
  Avatar,
1162
1834
  Badge,
1163
1835
  Box,
@@ -1172,6 +1844,7 @@ export {
1172
1844
  Row,
1173
1845
  Spacer,
1174
1846
  Stack,
1847
+ Tabs,
1175
1848
  Text,
1176
1849
  Toggle,
1177
1850
  UGCContainer,