@safe-ugc-ui/react 0.5.1 → 1.0.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,8 +1,10 @@
1
1
  // src/UGCRenderer.tsx
2
- import { useMemo as useMemo2 } 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
5
 
5
6
  // src/UGCContainer.tsx
7
+ import { forwardRef } from "react";
6
8
  import { jsx } from "react/jsx-runtime";
7
9
  var containerStyle = {
8
10
  overflow: "hidden",
@@ -10,10 +12,12 @@ var containerStyle = {
10
12
  contain: "content",
11
13
  position: "relative"
12
14
  };
13
- function UGCContainer({ children, style }) {
14
- const mergedStyle = style ? { ...containerStyle, ...style } : containerStyle;
15
- return /* @__PURE__ */ jsx("div", { style: mergedStyle, children });
16
- }
15
+ var UGCContainer = forwardRef(
16
+ function UGCContainer2({ children, style }, ref) {
17
+ const mergedStyle = style ? { ...containerStyle, ...style } : containerStyle;
18
+ return /* @__PURE__ */ jsx("div", { ref, style: mergedStyle, children });
19
+ }
20
+ );
17
21
 
18
22
  // src/node-renderer.tsx
19
23
  import {
@@ -25,7 +29,9 @@ import {
25
29
  } from "@safe-ugc-ui/types";
26
30
 
27
31
  // src/state-resolver.ts
28
- import { PROTOTYPE_POLLUTION_SEGMENTS } from "@safe-ugc-ui/types";
32
+ import {
33
+ PROTOTYPE_POLLUTION_SEGMENTS
34
+ } from "@safe-ugc-ui/types";
29
35
  function parseRefSegments(path) {
30
36
  const segments = [];
31
37
  const dotParts = path.split(".");
@@ -85,6 +91,111 @@ function resolveValue(value, state, locals) {
85
91
  }
86
92
  return value;
87
93
  }
94
+ function stringifyTextScalar(value) {
95
+ if (value === void 0) return "";
96
+ if (value === null) return "null";
97
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
98
+ return String(value);
99
+ }
100
+ return "";
101
+ }
102
+ function isTemplateObject(value) {
103
+ return typeof value === "object" && value !== null && "$template" in value && Array.isArray(value.$template);
104
+ }
105
+ function resolveTemplate(value, state, locals) {
106
+ if (!isTemplateObject(value)) {
107
+ return void 0;
108
+ }
109
+ return value.$template.map((part) => stringifyTextScalar(resolveValue(part, state, locals))).join("");
110
+ }
111
+ function resolveTextValue(value, state, locals) {
112
+ const template = resolveTemplate(value, state, locals);
113
+ if (template !== void 0) {
114
+ return template;
115
+ }
116
+ return stringifyTextScalar(resolveValue(value, state, locals));
117
+ }
118
+
119
+ // src/condition-resolver.ts
120
+ import { MAX_CONDITION_DEPTH, isRef } from "@safe-ugc-ui/types";
121
+ function resolveOperand(operand, state, locals) {
122
+ if (isRef(operand)) {
123
+ const resolved = resolveRef(operand.$ref, state, locals);
124
+ if (resolved === null || typeof resolved === "string" || typeof resolved === "number" || typeof resolved === "boolean") {
125
+ return resolved;
126
+ }
127
+ return void 0;
128
+ }
129
+ return operand;
130
+ }
131
+ function compareValues(op, left, right) {
132
+ if (left === void 0 || right === void 0) {
133
+ return false;
134
+ }
135
+ if (op === "eq") return left === right;
136
+ if (op === "ne") return left !== right;
137
+ if (left === null || right === null) {
138
+ return false;
139
+ }
140
+ if (typeof left !== typeof right) {
141
+ return false;
142
+ }
143
+ if (typeof left !== "number" && typeof left !== "string" || typeof right !== "number" && typeof right !== "string") {
144
+ return false;
145
+ }
146
+ switch (op) {
147
+ case "gt":
148
+ return left > right;
149
+ case "gte":
150
+ return left >= right;
151
+ case "lt":
152
+ return left < right;
153
+ case "lte":
154
+ return left <= right;
155
+ default:
156
+ return false;
157
+ }
158
+ }
159
+ function evaluateCondition(condition, state, locals, depth = 1) {
160
+ if (depth > MAX_CONDITION_DEPTH) {
161
+ return false;
162
+ }
163
+ if (typeof condition === "boolean") {
164
+ return condition;
165
+ }
166
+ if (isRef(condition)) {
167
+ return resolveRef(condition.$ref, state, locals) === true;
168
+ }
169
+ if (typeof condition !== "object" || condition === null || Array.isArray(condition)) {
170
+ return false;
171
+ }
172
+ const conditionObj = condition;
173
+ switch (conditionObj.op) {
174
+ case "not":
175
+ return !evaluateCondition(conditionObj.value, state, locals, depth + 1);
176
+ case "and":
177
+ return Array.isArray(conditionObj.values) && conditionObj.values.every(
178
+ (value) => evaluateCondition(value, state, locals, depth + 1)
179
+ );
180
+ case "or":
181
+ return Array.isArray(conditionObj.values) && conditionObj.values.some(
182
+ (value) => evaluateCondition(value, state, locals, depth + 1)
183
+ );
184
+ case "eq":
185
+ case "ne":
186
+ case "gt":
187
+ case "gte":
188
+ case "lt":
189
+ case "lte":
190
+ return compareValues(
191
+ conditionObj.op,
192
+ resolveOperand(conditionObj.left, state, locals),
193
+ resolveOperand(conditionObj.right, state, locals)
194
+ );
195
+ default:
196
+ return false;
197
+ }
198
+ }
88
199
 
89
200
  // src/style-mapper.ts
90
201
  import { ALLOWED_TRANSITION_PROPERTIES } from "@safe-ugc-ui/types";
@@ -99,6 +210,7 @@ var DIRECT_MAP_PROPS = [
99
210
  "gap",
100
211
  "width",
101
212
  "height",
213
+ "aspectRatio",
102
214
  "minWidth",
103
215
  "maxWidth",
104
216
  "minHeight",
@@ -155,6 +267,19 @@ function containsForbiddenCssFunction(value) {
155
267
  const lower = value.toLowerCase();
156
268
  return FORBIDDEN_CSS_FUNCTIONS_LOWER.some((fn) => lower.includes(fn));
157
269
  }
270
+ function isValidAspectRatio(value) {
271
+ if (typeof value === "number") {
272
+ return Number.isFinite(value) && value > 0;
273
+ }
274
+ if (typeof value !== "string" || containsForbiddenCssFunction(value)) {
275
+ return false;
276
+ }
277
+ const match = value.match(/^\s*([0-9]+(?:\.[0-9]+)?)\s*\/\s*([0-9]+(?:\.[0-9]+)?)\s*$/);
278
+ if (!match) {
279
+ return false;
280
+ }
281
+ return Number(match[1]) > 0 && Number(match[2]) > 0;
282
+ }
158
283
  function resolveStyleValue(value, state, locals) {
159
284
  return resolveValue(value, state, locals);
160
285
  }
@@ -365,6 +490,9 @@ function mapStyle(style, state, locals) {
365
490
  if (typeof resolved === "string" && containsForbiddenCssFunction(resolved)) {
366
491
  continue;
367
492
  }
493
+ if (prop === "aspectRatio" && !isValidAspectRatio(resolved)) {
494
+ continue;
495
+ }
368
496
  css[prop] = resolved;
369
497
  }
370
498
  }
@@ -516,9 +644,40 @@ function Column({ style, hoverStyle, children }) {
516
644
 
517
645
  // src/components/Text.tsx
518
646
  import { jsx as jsx5 } from "react/jsx-runtime";
519
- function Text({ content, style, hoverStyle }) {
647
+ function getClampStyle(maxLines, truncate) {
648
+ if (!maxLines || maxLines < 1) {
649
+ return void 0;
650
+ }
651
+ if (maxLines === 1) {
652
+ return {
653
+ display: "inline-block",
654
+ maxWidth: "100%",
655
+ overflow: "hidden",
656
+ whiteSpace: "nowrap",
657
+ textOverflow: truncate ?? "ellipsis"
658
+ };
659
+ }
660
+ return {
661
+ display: "-webkit-box",
662
+ maxWidth: "100%",
663
+ overflow: "hidden",
664
+ textOverflow: truncate ?? "ellipsis",
665
+ WebkitBoxOrient: "vertical",
666
+ WebkitLineClamp: maxLines
667
+ };
668
+ }
669
+ function Text({
670
+ content,
671
+ spans,
672
+ maxLines,
673
+ truncate,
674
+ style,
675
+ hoverStyle
676
+ }) {
520
677
  const { style: resolvedStyle, onMouseEnter, onMouseLeave } = useHoverStyle(style, hoverStyle);
521
- return /* @__PURE__ */ jsx5("span", { style: resolvedStyle, onMouseEnter, onMouseLeave, children: content });
678
+ const clampStyle = getClampStyle(maxLines, truncate);
679
+ const mergedStyle = clampStyle ? { ...resolvedStyle, ...clampStyle } : resolvedStyle;
680
+ return /* @__PURE__ */ jsx5("span", { style: mergedStyle, onMouseEnter, onMouseLeave, children: spans?.map((span, index) => /* @__PURE__ */ jsx5("span", { style: span.style, children: span.text }, index)) ?? content ?? "" });
522
681
  }
523
682
 
524
683
  // src/components/Image.tsx
@@ -751,13 +910,279 @@ function Toggle({ value, onToggle, onAction, disabled, style, hoverStyle }) {
751
910
  );
752
911
  }
753
912
 
913
+ // src/components/Accordion.tsx
914
+ import { useId, useState as useState2 } from "react";
915
+ import { jsx as jsx18, jsxs } from "react/jsx-runtime";
916
+ function getInitialExpandedIds(items, defaultExpanded, allowMultiple) {
917
+ const enabledIds = new Set(
918
+ items.filter((item) => item.disabled !== true).map((item) => item.id)
919
+ );
920
+ const initial = (defaultExpanded ?? []).filter((id) => enabledIds.has(id));
921
+ return allowMultiple ? initial : initial.slice(0, 1);
922
+ }
923
+ function Accordion({
924
+ items,
925
+ allowMultiple = false,
926
+ defaultExpanded,
927
+ style,
928
+ hoverStyle
929
+ }) {
930
+ const { style: resolvedStyle, onMouseEnter, onMouseLeave } = useHoverStyle(style, hoverStyle);
931
+ const [expandedIds, setExpandedIds] = useState2(
932
+ () => getInitialExpandedIds(items, defaultExpanded, allowMultiple)
933
+ );
934
+ const baseId = useId();
935
+ const expandedSet = new Set(expandedIds);
936
+ const toggleItem = (itemId, disabled) => {
937
+ if (disabled) {
938
+ return;
939
+ }
940
+ setExpandedIds((current) => {
941
+ const currentSet = new Set(current);
942
+ if (allowMultiple) {
943
+ if (currentSet.has(itemId)) {
944
+ currentSet.delete(itemId);
945
+ } else {
946
+ currentSet.add(itemId);
947
+ }
948
+ return [...currentSet];
949
+ }
950
+ return currentSet.has(itemId) ? [] : [itemId];
951
+ });
952
+ };
953
+ return /* @__PURE__ */ jsx18("div", { style: resolvedStyle, onMouseEnter, onMouseLeave, children: items.map((item) => {
954
+ const expanded = expandedSet.has(item.id);
955
+ const buttonId = `${baseId}-${item.id}-button`;
956
+ const panelId = `${baseId}-${item.id}-panel`;
957
+ return /* @__PURE__ */ jsxs(
958
+ "div",
959
+ {
960
+ style: {
961
+ borderTop: "1px solid rgba(148, 163, 184, 0.25)"
962
+ },
963
+ children: [
964
+ /* @__PURE__ */ jsxs(
965
+ "button",
966
+ {
967
+ id: buttonId,
968
+ type: "button",
969
+ "aria-expanded": expanded,
970
+ "aria-controls": panelId,
971
+ disabled: item.disabled,
972
+ onClick: () => toggleItem(item.id, item.disabled),
973
+ style: {
974
+ width: "100%",
975
+ display: "flex",
976
+ alignItems: "center",
977
+ justifyContent: "space-between",
978
+ padding: "12px 0",
979
+ background: "transparent",
980
+ border: "none",
981
+ color: "inherit",
982
+ textAlign: "left",
983
+ cursor: item.disabled ? "default" : "pointer",
984
+ opacity: item.disabled ? 0.5 : 1
985
+ },
986
+ children: [
987
+ /* @__PURE__ */ jsx18("span", { children: item.label }),
988
+ /* @__PURE__ */ jsx18("span", { "aria-hidden": "true", children: expanded ? "-" : "+" })
989
+ ]
990
+ }
991
+ ),
992
+ expanded ? /* @__PURE__ */ jsx18(
993
+ "div",
994
+ {
995
+ id: panelId,
996
+ role: "region",
997
+ "aria-labelledby": buttonId,
998
+ style: { paddingBottom: 12 },
999
+ children: item.content
1000
+ }
1001
+ ) : null
1002
+ ]
1003
+ },
1004
+ item.id
1005
+ );
1006
+ }) });
1007
+ }
1008
+
1009
+ // src/components/Tabs.tsx
1010
+ import {
1011
+ useEffect,
1012
+ useId as useId2,
1013
+ useRef,
1014
+ useState as useState3
1015
+ } from "react";
1016
+ import { jsx as jsx19, jsxs as jsxs2 } from "react/jsx-runtime";
1017
+ function getEnabledTabIds(tabs) {
1018
+ return tabs.filter((tab) => tab.disabled !== true).map((tab) => tab.id);
1019
+ }
1020
+ function getInitialSelectedTab(tabs, defaultTab) {
1021
+ const enabledIds = getEnabledTabIds(tabs);
1022
+ if (enabledIds.length === 0) {
1023
+ return void 0;
1024
+ }
1025
+ if (defaultTab && enabledIds.includes(defaultTab)) {
1026
+ return defaultTab;
1027
+ }
1028
+ return enabledIds[0];
1029
+ }
1030
+ function getNextEnabledIndex(tabs, startIndex, direction) {
1031
+ for (let offset = 1; offset <= tabs.length; offset++) {
1032
+ const nextIndex = (startIndex + direction * offset + tabs.length) % tabs.length;
1033
+ if (tabs[nextIndex]?.disabled !== true) {
1034
+ return nextIndex;
1035
+ }
1036
+ }
1037
+ return startIndex;
1038
+ }
1039
+ function focusTab(buttonRefs, index) {
1040
+ buttonRefs.current[index]?.focus();
1041
+ }
1042
+ function Tabs({
1043
+ tabs,
1044
+ defaultTab,
1045
+ style,
1046
+ hoverStyle
1047
+ }) {
1048
+ const { style: resolvedStyle, onMouseEnter, onMouseLeave } = useHoverStyle(style, hoverStyle);
1049
+ const [selectedTab, setSelectedTab] = useState3(
1050
+ () => getInitialSelectedTab(tabs, defaultTab)
1051
+ );
1052
+ const baseId = useId2();
1053
+ const buttonRefs = useRef([]);
1054
+ useEffect(() => {
1055
+ setSelectedTab((current) => {
1056
+ const enabledIds = getEnabledTabIds(tabs);
1057
+ if (enabledIds.length === 0) {
1058
+ return void 0;
1059
+ }
1060
+ if (current && enabledIds.includes(current)) {
1061
+ return current;
1062
+ }
1063
+ if (defaultTab && enabledIds.includes(defaultTab)) {
1064
+ return defaultTab;
1065
+ }
1066
+ return enabledIds[0];
1067
+ });
1068
+ }, [tabs, defaultTab]);
1069
+ const selectedItem = tabs.find((tab) => tab.id === selectedTab && tab.disabled !== true) ?? tabs.find((tab) => tab.disabled !== true);
1070
+ const activateTab = (index, focus = false) => {
1071
+ const tab = tabs[index];
1072
+ if (!tab || tab.disabled) {
1073
+ return;
1074
+ }
1075
+ setSelectedTab(tab.id);
1076
+ if (focus) {
1077
+ focusTab(buttonRefs, index);
1078
+ }
1079
+ };
1080
+ const onTabKeyDown = (event, index) => {
1081
+ switch (event.key) {
1082
+ case "ArrowRight":
1083
+ case "ArrowDown": {
1084
+ event.preventDefault();
1085
+ activateTab(getNextEnabledIndex(tabs, index, 1), true);
1086
+ break;
1087
+ }
1088
+ case "ArrowLeft":
1089
+ case "ArrowUp": {
1090
+ event.preventDefault();
1091
+ activateTab(getNextEnabledIndex(tabs, index, -1), true);
1092
+ break;
1093
+ }
1094
+ case "Home": {
1095
+ event.preventDefault();
1096
+ const firstEnabledIndex = tabs.findIndex((tab) => tab.disabled !== true);
1097
+ if (firstEnabledIndex >= 0) {
1098
+ activateTab(firstEnabledIndex, true);
1099
+ }
1100
+ break;
1101
+ }
1102
+ case "End": {
1103
+ event.preventDefault();
1104
+ const lastEnabledIndex = [...tabs].map((tab, currentIndex) => ({ tab, currentIndex })).reverse().find(({ tab }) => tab.disabled !== true);
1105
+ if (lastEnabledIndex) {
1106
+ activateTab(lastEnabledIndex.currentIndex, true);
1107
+ }
1108
+ break;
1109
+ }
1110
+ default:
1111
+ break;
1112
+ }
1113
+ };
1114
+ return /* @__PURE__ */ jsxs2("div", { style: resolvedStyle, onMouseEnter, onMouseLeave, children: [
1115
+ /* @__PURE__ */ jsx19(
1116
+ "div",
1117
+ {
1118
+ role: "tablist",
1119
+ "aria-orientation": "horizontal",
1120
+ style: {
1121
+ display: "flex",
1122
+ gap: 8,
1123
+ borderBottom: "1px solid rgba(148, 163, 184, 0.25)",
1124
+ paddingBottom: 8
1125
+ },
1126
+ children: tabs.map((tab, index) => {
1127
+ const selected = selectedItem?.id === tab.id;
1128
+ const tabId = `${baseId}-${tab.id}-tab`;
1129
+ const panelId = `${baseId}-${tab.id}-panel`;
1130
+ return /* @__PURE__ */ jsx19(
1131
+ "button",
1132
+ {
1133
+ ref: (element) => {
1134
+ buttonRefs.current[index] = element;
1135
+ },
1136
+ id: tabId,
1137
+ type: "button",
1138
+ role: "tab",
1139
+ "aria-selected": selected,
1140
+ "aria-controls": panelId,
1141
+ disabled: tab.disabled,
1142
+ tabIndex: selected ? 0 : -1,
1143
+ onClick: () => activateTab(index),
1144
+ onKeyDown: (event) => onTabKeyDown(event, index),
1145
+ style: {
1146
+ padding: "8px 12px",
1147
+ borderRadius: 8,
1148
+ border: "none",
1149
+ background: selected ? "rgba(148, 163, 184, 0.16)" : "transparent",
1150
+ color: "inherit",
1151
+ cursor: tab.disabled ? "default" : "pointer",
1152
+ opacity: tab.disabled ? 0.5 : 1,
1153
+ fontWeight: selected ? 600 : 400
1154
+ },
1155
+ children: tab.label
1156
+ },
1157
+ tab.id
1158
+ );
1159
+ })
1160
+ }
1161
+ ),
1162
+ selectedItem ? /* @__PURE__ */ jsx19(
1163
+ "div",
1164
+ {
1165
+ id: `${baseId}-${selectedItem.id}-panel`,
1166
+ role: "tabpanel",
1167
+ "aria-labelledby": `${baseId}-${selectedItem.id}-tab`,
1168
+ style: { paddingTop: 12 },
1169
+ children: selectedItem.content
1170
+ }
1171
+ ) : null
1172
+ ] });
1173
+ }
1174
+
754
1175
  // src/node-renderer.tsx
755
- import { jsx as jsx18 } from "react/jsx-runtime";
1176
+ import { jsx as jsx20 } from "react/jsx-runtime";
756
1177
  function isForLoop(obj) {
757
1178
  if (obj == null || typeof obj !== "object" || Array.isArray(obj)) return false;
758
1179
  const o = obj;
759
1180
  return typeof o.for === "string" && typeof o.in === "string" && o.template != null;
760
1181
  }
1182
+ function isFragmentUse(obj) {
1183
+ if (obj == null || typeof obj !== "object" || Array.isArray(obj)) return false;
1184
+ return typeof obj.$use === "string";
1185
+ }
761
1186
  function utf8ByteLength(str) {
762
1187
  let bytes = 0;
763
1188
  for (let i = 0; i < str.length; i++) {
@@ -810,20 +1235,155 @@ function mergeStyleWithCardStyles(nodeStyle, cardStyles) {
810
1235
  hoverStyle: mergedHoverStyle
811
1236
  };
812
1237
  }
1238
+ function getCompactResponsiveStyle(nodeResponsive) {
1239
+ if (!nodeResponsive) return void 0;
1240
+ const compact = nodeResponsive.compact;
1241
+ if (compact == null || typeof compact !== "object" || Array.isArray(compact)) {
1242
+ return void 0;
1243
+ }
1244
+ return compact;
1245
+ }
1246
+ function mergeEffectiveNodeStyle(node, ctx) {
1247
+ const baseStyle = mergeStyleWithCardStyles(node.style, ctx.cardStyles);
1248
+ if (!ctx.responsive.compact) {
1249
+ return baseStyle;
1250
+ }
1251
+ const compactOverride = mergeNamedStyle(
1252
+ getCompactResponsiveStyle(node.responsive),
1253
+ ctx.cardStyles
1254
+ );
1255
+ if (!compactOverride) {
1256
+ return baseStyle;
1257
+ }
1258
+ const {
1259
+ hoverStyle: _hoverStyle,
1260
+ transition: _transition,
1261
+ ...compactStyleWithoutInteractiveFields
1262
+ } = compactOverride;
1263
+ return {
1264
+ ...baseStyle ?? {},
1265
+ ...compactStyleWithoutInteractiveFields
1266
+ };
1267
+ }
1268
+ function resolveTextPayload(node, ctx) {
1269
+ const rawSpans = Array.isArray(node.spans) ? node.spans : void 0;
1270
+ if (rawSpans) {
1271
+ const spans = rawSpans.map((span) => {
1272
+ const rawSpan = span;
1273
+ const spanStyle = rawSpan.style != null && typeof rawSpan.style === "object" && !Array.isArray(rawSpan.style) ? rawSpan.style : void 0;
1274
+ return {
1275
+ text: resolveTextValue(rawSpan.text, ctx.state, ctx.locals),
1276
+ style: spanStyle ? mapStyle(spanStyle, ctx.state, ctx.locals) : void 0,
1277
+ rawStyleBytes: spanStyle ? utf8ByteLength(JSON.stringify(spanStyle)) : 0
1278
+ };
1279
+ });
1280
+ const combinedText = spans.map((span) => span.text).join("");
1281
+ return {
1282
+ spans: spans.map(({ rawStyleBytes: _rawStyleBytes, ...span }) => span),
1283
+ textBytes: utf8ByteLength(combinedText),
1284
+ styleBytes: spans.reduce((sum, span) => sum + span.rawStyleBytes, 0)
1285
+ };
1286
+ }
1287
+ const content = resolveTextValue(node.content, ctx.state, ctx.locals);
1288
+ return {
1289
+ content,
1290
+ textBytes: utf8ByteLength(content),
1291
+ styleBytes: 0
1292
+ };
1293
+ }
1294
+ function resolveAccordionItems(node, ctx, key) {
1295
+ const rawItems = Array.isArray(node.items) ? node.items : [];
1296
+ return rawItems.flatMap((item, index) => {
1297
+ if (item == null || typeof item !== "object" || Array.isArray(item)) {
1298
+ return [];
1299
+ }
1300
+ const rawItem = item;
1301
+ const id = typeof rawItem.id === "string" ? rawItem.id : `item-${index}`;
1302
+ const label = resolveTextValue(rawItem.label, ctx.state, ctx.locals);
1303
+ const resolvedDisabled = resolveValue(rawItem.disabled, ctx.state, ctx.locals);
1304
+ const disabled = typeof resolvedDisabled === "boolean" ? resolvedDisabled : void 0;
1305
+ const content = renderNode(
1306
+ rawItem.content,
1307
+ ctx,
1308
+ `${String(key)}.items[${index}].content`
1309
+ );
1310
+ return [{ id, label, content, disabled }];
1311
+ });
1312
+ }
1313
+ function resolveTabsItems(node, ctx, key) {
1314
+ const rawTabs = Array.isArray(node.tabs) ? node.tabs : [];
1315
+ return rawTabs.flatMap((tab, index) => {
1316
+ if (tab == null || typeof tab !== "object" || Array.isArray(tab)) {
1317
+ return [];
1318
+ }
1319
+ const rawTab = tab;
1320
+ const id = typeof rawTab.id === "string" ? rawTab.id : `tab-${index}`;
1321
+ const label = resolveTextValue(rawTab.label, ctx.state, ctx.locals);
1322
+ const resolvedDisabled = resolveValue(rawTab.disabled, ctx.state, ctx.locals);
1323
+ const disabled = typeof resolvedDisabled === "boolean" ? resolvedDisabled : void 0;
1324
+ const content = renderNode(
1325
+ rawTab.content,
1326
+ ctx,
1327
+ `${String(key)}.tabs[${index}].content`
1328
+ );
1329
+ return [{ id, label, content, disabled }];
1330
+ });
1331
+ }
813
1332
  function renderNode(node, ctx, key) {
814
1333
  if (node == null || typeof node !== "object") return null;
1334
+ if (isFragmentUse(node)) {
1335
+ if ("$if" in node && !evaluateCondition(node.$if, ctx.state, ctx.locals)) {
1336
+ return null;
1337
+ }
1338
+ if ((ctx.fragmentStack?.length ?? 0) > 0) {
1339
+ ctx.onError?.([{
1340
+ code: "RUNTIME_FRAGMENT_NESTED_USE",
1341
+ message: 'Fragments may not contain nested "$use" references',
1342
+ path: String(key)
1343
+ }]);
1344
+ return null;
1345
+ }
1346
+ if (ctx.fragmentStack?.includes(node.$use)) {
1347
+ ctx.onError?.([{
1348
+ code: "RUNTIME_FRAGMENT_CYCLE",
1349
+ message: `Fragment "${node.$use}" recursively references itself`,
1350
+ path: String(key)
1351
+ }]);
1352
+ return null;
1353
+ }
1354
+ const fragment = ctx.fragments?.[node.$use];
1355
+ if (fragment == null) {
1356
+ ctx.onError?.([{
1357
+ code: "RUNTIME_FRAGMENT_NOT_FOUND",
1358
+ message: `Fragment "${node.$use}" was not found`,
1359
+ path: String(key)
1360
+ }]);
1361
+ return null;
1362
+ }
1363
+ return renderNode(
1364
+ fragment,
1365
+ {
1366
+ ...ctx,
1367
+ fragmentStack: [...ctx.fragmentStack ?? [], node.$use]
1368
+ },
1369
+ key
1370
+ );
1371
+ }
815
1372
  const n = node;
816
1373
  if (!n.type) return null;
817
- const mergedRawStyle = mergeStyleWithCardStyles(n.style, ctx.cardStyles);
1374
+ if ("$if" in n && !evaluateCondition(n.$if, ctx.state, ctx.locals)) {
1375
+ return null;
1376
+ }
1377
+ const mergedRawStyle = mergeEffectiveNodeStyle(n, ctx);
818
1378
  const rv = (val) => resolveValue(val, ctx.state, ctx.locals);
819
- const styleDelta = mergedRawStyle ? utf8ByteLength(JSON.stringify(mergedRawStyle)) : 0;
1379
+ let styleDelta = mergedRawStyle ? utf8ByteLength(JSON.stringify(mergedRawStyle)) : 0;
820
1380
  const overflowDelta = mergedRawStyle?.overflow === "auto" ? 1 : 0;
821
- let resolvedTextContent;
1381
+ let resolvedTextPayload;
822
1382
  let textDelta = 0;
823
1383
  if (n.type === "Text") {
824
- const raw = rv(n.content);
825
- resolvedTextContent = typeof raw === "string" ? raw : "";
826
- textDelta = utf8ByteLength(resolvedTextContent);
1384
+ resolvedTextPayload = resolveTextPayload(n, ctx);
1385
+ textDelta = resolvedTextPayload.textBytes;
1386
+ styleDelta += resolvedTextPayload.styleBytes;
827
1387
  }
828
1388
  if (ctx.limits.nodeCount + 1 > MAX_NODE_COUNT) {
829
1389
  ctx.onError?.([{ code: "RUNTIME_NODE_LIMIT", message: `Node count exceeds maximum of ${MAX_NODE_COUNT}`, path: String(key) }]);
@@ -851,17 +1411,31 @@ function renderNode(node, ctx, key) {
851
1411
  const childElements = renderChildren(n.children, ctx);
852
1412
  switch (n.type) {
853
1413
  case "Box":
854
- return /* @__PURE__ */ jsx18(Box, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1414
+ return /* @__PURE__ */ jsx20(Box, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
855
1415
  case "Row":
856
- return /* @__PURE__ */ jsx18(Row, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1416
+ return /* @__PURE__ */ jsx20(Row, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
857
1417
  case "Column":
858
- return /* @__PURE__ */ jsx18(Column, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1418
+ return /* @__PURE__ */ jsx20(Column, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
859
1419
  case "Stack":
860
- return /* @__PURE__ */ jsx18(Stack, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1420
+ return /* @__PURE__ */ jsx20(Stack, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
861
1421
  case "Grid":
862
- return /* @__PURE__ */ jsx18(Grid, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
863
- case "Text":
864
- return /* @__PURE__ */ jsx18(Text, { content: resolvedTextContent, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1422
+ return /* @__PURE__ */ jsx20(Grid, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1423
+ case "Text": {
1424
+ const maxLines = typeof n.maxLines === "number" ? n.maxLines : void 0;
1425
+ const truncate = n.truncate === "clip" || n.truncate === "ellipsis" ? n.truncate : void 0;
1426
+ return /* @__PURE__ */ jsx20(
1427
+ Text,
1428
+ {
1429
+ content: resolvedTextPayload?.content,
1430
+ spans: resolvedTextPayload?.spans,
1431
+ maxLines,
1432
+ truncate,
1433
+ style: cssStyle,
1434
+ hoverStyle: cssHoverStyle
1435
+ },
1436
+ key
1437
+ );
1438
+ }
865
1439
  case "Image": {
866
1440
  let src = rv(n.src);
867
1441
  if (typeof src !== "string" || !src) return null;
@@ -872,7 +1446,7 @@ function renderNode(node, ctx, key) {
872
1446
  if (typeof resolved === "string" && resolved.trim().toLowerCase().startsWith("javascript:")) return null;
873
1447
  const resolvedAlt = rv(n.alt);
874
1448
  const alt = typeof resolvedAlt === "string" ? resolvedAlt : void 0;
875
- return /* @__PURE__ */ jsx18(Image, { src: resolved, alt, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1449
+ return /* @__PURE__ */ jsx20(Image, { src: resolved, alt, style: cssStyle, hoverStyle: cssHoverStyle }, key);
876
1450
  }
877
1451
  case "Avatar": {
878
1452
  let src = rv(n.src);
@@ -884,7 +1458,7 @@ function renderNode(node, ctx, key) {
884
1458
  if (typeof resolved === "string" && resolved.trim().toLowerCase().startsWith("javascript:")) return null;
885
1459
  const resolvedSize = rv(n.size);
886
1460
  const size = typeof resolvedSize === "number" || typeof resolvedSize === "string" ? resolvedSize : void 0;
887
- return /* @__PURE__ */ jsx18(Avatar, { src: resolved, size, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1461
+ return /* @__PURE__ */ jsx20(Avatar, { src: resolved, size, style: cssStyle, hoverStyle: cssHoverStyle }, key);
888
1462
  }
889
1463
  case "Icon": {
890
1464
  const resolvedName = rv(n.name);
@@ -893,7 +1467,7 @@ function renderNode(node, ctx, key) {
893
1467
  const size = typeof resolvedSize === "number" || typeof resolvedSize === "string" ? resolvedSize : void 0;
894
1468
  const resolvedColor = rv(n.color);
895
1469
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
896
- return /* @__PURE__ */ jsx18(
1470
+ return /* @__PURE__ */ jsx20(
897
1471
  Icon,
898
1472
  {
899
1473
  name,
@@ -909,14 +1483,14 @@ function renderNode(node, ctx, key) {
909
1483
  case "Spacer": {
910
1484
  const resolvedSize = rv(n.size);
911
1485
  const size = typeof resolvedSize === "number" || typeof resolvedSize === "string" ? resolvedSize : void 0;
912
- return /* @__PURE__ */ jsx18(Spacer, { size, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1486
+ return /* @__PURE__ */ jsx20(Spacer, { size, style: cssStyle, hoverStyle: cssHoverStyle }, key);
913
1487
  }
914
1488
  case "Divider": {
915
1489
  const resolvedColor = rv(n.color);
916
1490
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
917
1491
  const resolvedThickness = rv(n.thickness);
918
1492
  const thickness = typeof resolvedThickness === "number" || typeof resolvedThickness === "string" ? resolvedThickness : void 0;
919
- return /* @__PURE__ */ jsx18(Divider, { color, thickness, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1493
+ return /* @__PURE__ */ jsx20(Divider, { color, thickness, style: cssStyle, hoverStyle: cssHoverStyle }, key);
920
1494
  }
921
1495
  case "ProgressBar": {
922
1496
  const resolvedValue = rv(n.value);
@@ -925,33 +1499,33 @@ function renderNode(node, ctx, key) {
925
1499
  const max = typeof resolvedMax === "number" ? resolvedMax : 100;
926
1500
  const resolvedColor = rv(n.color);
927
1501
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
928
- return /* @__PURE__ */ jsx18(ProgressBar, { value, max, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1502
+ return /* @__PURE__ */ jsx20(ProgressBar, { value, max, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
929
1503
  }
930
1504
  case "Badge": {
931
- const resolvedLabel = rv(n.label);
932
- const label = typeof resolvedLabel === "string" ? resolvedLabel : "";
1505
+ const label = resolveTextValue(n.label, ctx.state, ctx.locals);
933
1506
  const resolvedColor = rv(n.color);
934
1507
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
935
- return /* @__PURE__ */ jsx18(Badge, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1508
+ return /* @__PURE__ */ jsx20(Badge, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
936
1509
  }
937
1510
  case "Chip": {
938
- const resolvedLabel = rv(n.label);
939
- const label = typeof resolvedLabel === "string" ? resolvedLabel : "";
1511
+ const label = resolveTextValue(n.label, ctx.state, ctx.locals);
940
1512
  const resolvedColor = rv(n.color);
941
1513
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
942
- return /* @__PURE__ */ jsx18(Chip, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1514
+ return /* @__PURE__ */ jsx20(Chip, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
943
1515
  }
944
1516
  case "Button": {
945
- const resolvedLabel = rv(n.label);
946
- const label = typeof resolvedLabel === "string" ? resolvedLabel : "";
1517
+ const label = resolveTextValue(n.label, ctx.state, ctx.locals);
947
1518
  const actionVal = n.action;
948
1519
  const action = typeof actionVal === "string" ? actionVal : "";
949
- return /* @__PURE__ */ jsx18(
1520
+ const resolvedDisabled = rv(n.disabled);
1521
+ const disabled = typeof resolvedDisabled === "boolean" ? resolvedDisabled : void 0;
1522
+ return /* @__PURE__ */ jsx20(
950
1523
  Button,
951
1524
  {
952
1525
  label,
953
1526
  action,
954
1527
  onAction: ctx.onAction,
1528
+ disabled,
955
1529
  style: cssStyle,
956
1530
  hoverStyle: cssHoverStyle
957
1531
  },
@@ -963,12 +1537,47 @@ function renderNode(node, ctx, key) {
963
1537
  const value = typeof resolvedValue === "boolean" ? resolvedValue : false;
964
1538
  const onToggleVal = n.onToggle;
965
1539
  const onToggle = typeof onToggleVal === "string" ? onToggleVal : "";
966
- return /* @__PURE__ */ jsx18(
1540
+ const resolvedDisabled = rv(n.disabled);
1541
+ const disabled = typeof resolvedDisabled === "boolean" ? resolvedDisabled : void 0;
1542
+ return /* @__PURE__ */ jsx20(
967
1543
  Toggle,
968
1544
  {
969
1545
  value,
970
1546
  onToggle,
971
1547
  onAction: ctx.onAction,
1548
+ disabled,
1549
+ style: cssStyle,
1550
+ hoverStyle: cssHoverStyle
1551
+ },
1552
+ key
1553
+ );
1554
+ }
1555
+ case "Accordion": {
1556
+ const items = resolveAccordionItems(n, ctx, key);
1557
+ const allowMultiple = n.allowMultiple === true;
1558
+ const defaultExpanded = Array.isArray(n.defaultExpanded) ? n.defaultExpanded.filter(
1559
+ (value) => typeof value === "string"
1560
+ ) : void 0;
1561
+ return /* @__PURE__ */ jsx20(
1562
+ Accordion,
1563
+ {
1564
+ items,
1565
+ allowMultiple,
1566
+ defaultExpanded,
1567
+ style: cssStyle,
1568
+ hoverStyle: cssHoverStyle
1569
+ },
1570
+ key
1571
+ );
1572
+ }
1573
+ case "Tabs": {
1574
+ const tabs = resolveTabsItems(n, ctx, key);
1575
+ const defaultTab = typeof n.defaultTab === "string" ? n.defaultTab : void 0;
1576
+ return /* @__PURE__ */ jsx20(
1577
+ Tabs,
1578
+ {
1579
+ tabs,
1580
+ defaultTab,
972
1581
  style: cssStyle,
973
1582
  hoverStyle: cssHoverStyle
974
1583
  },
@@ -1016,7 +1625,7 @@ function renderForLoop(loop, ctx) {
1016
1625
  }
1017
1626
  return elements;
1018
1627
  }
1019
- function renderTree(rootNode, state, assets, cardStyles, iconResolver, onAction, onError) {
1628
+ function renderTree(rootNode, state, assets, cardStyles, iconResolver, onAction, onError, responsive = { compact: false }, fragments) {
1020
1629
  const limits = {
1021
1630
  nodeCount: 0,
1022
1631
  textBytes: 0,
@@ -1027,16 +1636,19 @@ function renderTree(rootNode, state, assets, cardStyles, iconResolver, onAction,
1027
1636
  state,
1028
1637
  assets,
1029
1638
  cardStyles,
1639
+ fragments,
1640
+ fragmentStack: [],
1030
1641
  iconResolver,
1031
1642
  onAction,
1032
1643
  onError,
1033
- limits
1644
+ limits,
1645
+ responsive
1034
1646
  };
1035
1647
  return renderNode(rootNode, ctx, "root");
1036
1648
  }
1037
1649
 
1038
1650
  // src/UGCRenderer.tsx
1039
- import { jsx as jsx19 } from "react/jsx-runtime";
1651
+ import { jsx as jsx21 } from "react/jsx-runtime";
1040
1652
  function UGCRenderer({
1041
1653
  card,
1042
1654
  viewName,
@@ -1047,6 +1659,10 @@ function UGCRenderer({
1047
1659
  iconResolver,
1048
1660
  onAction
1049
1661
  }) {
1662
+ const [containerElement, setContainerElement] = useState4(null);
1663
+ const [containerWidth, setContainerWidth] = useState4(
1664
+ typeof window === "undefined" ? null : window.innerWidth
1665
+ );
1050
1666
  const result = useMemo2(() => {
1051
1667
  const validationResult = typeof card === "string" ? validateRaw(card) : validate(card);
1052
1668
  if (!validationResult.valid) {
@@ -1064,30 +1680,65 @@ function UGCRenderer({
1064
1680
  ...stateOverride ?? {}
1065
1681
  };
1066
1682
  const cardStyles = cardObj.styles;
1683
+ const fragments = cardObj.fragments;
1067
1684
  return {
1068
1685
  valid: true,
1069
1686
  rootNode: views[selectedView],
1070
1687
  state: mergedState,
1071
- cardStyles
1688
+ cardStyles,
1689
+ fragments
1072
1690
  };
1073
1691
  }, [card, viewName, stateOverride]);
1692
+ useEffect2(() => {
1693
+ if (!containerElement) {
1694
+ return;
1695
+ }
1696
+ const updateWidth = (nextWidth) => {
1697
+ if (typeof nextWidth === "number" && Number.isFinite(nextWidth)) {
1698
+ setContainerWidth(nextWidth);
1699
+ return;
1700
+ }
1701
+ setContainerWidth(containerElement.getBoundingClientRect().width);
1702
+ };
1703
+ updateWidth();
1704
+ if (typeof ResizeObserver === "function") {
1705
+ const observer = new ResizeObserver((entries) => {
1706
+ const entry = entries[0];
1707
+ updateWidth(entry?.contentRect?.width);
1708
+ });
1709
+ observer.observe(containerElement);
1710
+ return () => observer.disconnect();
1711
+ }
1712
+ const handleResize = () => updateWidth();
1713
+ window.addEventListener("resize", handleResize);
1714
+ return () => window.removeEventListener("resize", handleResize);
1715
+ }, [containerElement]);
1716
+ const responsive = useMemo2(
1717
+ () => ({
1718
+ compact: containerWidth != null && containerWidth <= COMPACT_BREAKPOINT_MAX_WIDTH
1719
+ }),
1720
+ [containerWidth]
1721
+ );
1074
1722
  if (!result.valid) {
1075
1723
  if (onError && result.errors.length > 0) {
1076
1724
  onError(result.errors);
1077
1725
  }
1078
1726
  return null;
1079
1727
  }
1080
- return /* @__PURE__ */ jsx19(UGCContainer, { style: containerStyle2, children: renderTree(
1728
+ return /* @__PURE__ */ jsx21(UGCContainer, { ref: setContainerElement, style: containerStyle2, children: renderTree(
1081
1729
  result.rootNode,
1082
1730
  result.state,
1083
1731
  assets,
1084
1732
  result.cardStyles,
1085
1733
  iconResolver,
1086
1734
  onAction,
1087
- onError
1735
+ onError,
1736
+ responsive,
1737
+ result.fragments
1088
1738
  ) });
1089
1739
  }
1090
1740
  export {
1741
+ Accordion,
1091
1742
  Avatar,
1092
1743
  Badge,
1093
1744
  Box,
@@ -1102,6 +1753,7 @@ export {
1102
1753
  Row,
1103
1754
  Spacer,
1104
1755
  Stack,
1756
+ Tabs,
1105
1757
  Text,
1106
1758
  Toggle,
1107
1759
  UGCContainer,