@safe-ugc-ui/react 0.6.0 → 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,5 +1,5 @@
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
4
  import { COMPACT_BREAKPOINT_MAX_WIDTH } from "@safe-ugc-ui/types";
5
5
 
@@ -29,7 +29,9 @@ import {
29
29
  } from "@safe-ugc-ui/types";
30
30
 
31
31
  // src/state-resolver.ts
32
- import { PROTOTYPE_POLLUTION_SEGMENTS } from "@safe-ugc-ui/types";
32
+ import {
33
+ PROTOTYPE_POLLUTION_SEGMENTS
34
+ } from "@safe-ugc-ui/types";
33
35
  function parseRefSegments(path) {
34
36
  const segments = [];
35
37
  const dotParts = path.split(".");
@@ -89,6 +91,111 @@ function resolveValue(value, state, locals) {
89
91
  }
90
92
  return value;
91
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
+ }
92
199
 
93
200
  // src/style-mapper.ts
94
201
  import { ALLOWED_TRANSITION_PROPERTIES } from "@safe-ugc-ui/types";
@@ -103,6 +210,7 @@ var DIRECT_MAP_PROPS = [
103
210
  "gap",
104
211
  "width",
105
212
  "height",
213
+ "aspectRatio",
106
214
  "minWidth",
107
215
  "maxWidth",
108
216
  "minHeight",
@@ -159,6 +267,19 @@ function containsForbiddenCssFunction(value) {
159
267
  const lower = value.toLowerCase();
160
268
  return FORBIDDEN_CSS_FUNCTIONS_LOWER.some((fn) => lower.includes(fn));
161
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
+ }
162
283
  function resolveStyleValue(value, state, locals) {
163
284
  return resolveValue(value, state, locals);
164
285
  }
@@ -369,6 +490,9 @@ function mapStyle(style, state, locals) {
369
490
  if (typeof resolved === "string" && containsForbiddenCssFunction(resolved)) {
370
491
  continue;
371
492
  }
493
+ if (prop === "aspectRatio" && !isValidAspectRatio(resolved)) {
494
+ continue;
495
+ }
372
496
  css[prop] = resolved;
373
497
  }
374
498
  }
@@ -520,9 +644,40 @@ function Column({ style, hoverStyle, children }) {
520
644
 
521
645
  // src/components/Text.tsx
522
646
  import { jsx as jsx5 } from "react/jsx-runtime";
523
- 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
+ }) {
524
677
  const { style: resolvedStyle, onMouseEnter, onMouseLeave } = useHoverStyle(style, hoverStyle);
525
- 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 ?? "" });
526
681
  }
527
682
 
528
683
  // src/components/Image.tsx
@@ -755,13 +910,279 @@ function Toggle({ value, onToggle, onAction, disabled, style, hoverStyle }) {
755
910
  );
756
911
  }
757
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
+
758
1175
  // src/node-renderer.tsx
759
- import { jsx as jsx18 } from "react/jsx-runtime";
1176
+ import { jsx as jsx20 } from "react/jsx-runtime";
760
1177
  function isForLoop(obj) {
761
1178
  if (obj == null || typeof obj !== "object" || Array.isArray(obj)) return false;
762
1179
  const o = obj;
763
1180
  return typeof o.for === "string" && typeof o.in === "string" && o.template != null;
764
1181
  }
1182
+ function isFragmentUse(obj) {
1183
+ if (obj == null || typeof obj !== "object" || Array.isArray(obj)) return false;
1184
+ return typeof obj.$use === "string";
1185
+ }
765
1186
  function utf8ByteLength(str) {
766
1187
  let bytes = 0;
767
1188
  for (let i = 0; i < str.length; i++) {
@@ -844,20 +1265,125 @@ function mergeEffectiveNodeStyle(node, ctx) {
844
1265
  ...compactStyleWithoutInteractiveFields
845
1266
  };
846
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
+ }
847
1332
  function renderNode(node, ctx, key) {
848
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
+ }
849
1372
  const n = node;
850
1373
  if (!n.type) return null;
1374
+ if ("$if" in n && !evaluateCondition(n.$if, ctx.state, ctx.locals)) {
1375
+ return null;
1376
+ }
851
1377
  const mergedRawStyle = mergeEffectiveNodeStyle(n, ctx);
852
1378
  const rv = (val) => resolveValue(val, ctx.state, ctx.locals);
853
- const styleDelta = mergedRawStyle ? utf8ByteLength(JSON.stringify(mergedRawStyle)) : 0;
1379
+ let styleDelta = mergedRawStyle ? utf8ByteLength(JSON.stringify(mergedRawStyle)) : 0;
854
1380
  const overflowDelta = mergedRawStyle?.overflow === "auto" ? 1 : 0;
855
- let resolvedTextContent;
1381
+ let resolvedTextPayload;
856
1382
  let textDelta = 0;
857
1383
  if (n.type === "Text") {
858
- const raw = rv(n.content);
859
- resolvedTextContent = typeof raw === "string" ? raw : "";
860
- textDelta = utf8ByteLength(resolvedTextContent);
1384
+ resolvedTextPayload = resolveTextPayload(n, ctx);
1385
+ textDelta = resolvedTextPayload.textBytes;
1386
+ styleDelta += resolvedTextPayload.styleBytes;
861
1387
  }
862
1388
  if (ctx.limits.nodeCount + 1 > MAX_NODE_COUNT) {
863
1389
  ctx.onError?.([{ code: "RUNTIME_NODE_LIMIT", message: `Node count exceeds maximum of ${MAX_NODE_COUNT}`, path: String(key) }]);
@@ -885,17 +1411,31 @@ function renderNode(node, ctx, key) {
885
1411
  const childElements = renderChildren(n.children, ctx);
886
1412
  switch (n.type) {
887
1413
  case "Box":
888
- return /* @__PURE__ */ jsx18(Box, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1414
+ return /* @__PURE__ */ jsx20(Box, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
889
1415
  case "Row":
890
- return /* @__PURE__ */ jsx18(Row, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1416
+ return /* @__PURE__ */ jsx20(Row, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
891
1417
  case "Column":
892
- return /* @__PURE__ */ jsx18(Column, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1418
+ return /* @__PURE__ */ jsx20(Column, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
893
1419
  case "Stack":
894
- return /* @__PURE__ */ jsx18(Stack, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
1420
+ return /* @__PURE__ */ jsx20(Stack, { style: cssStyle, hoverStyle: cssHoverStyle, children: childElements }, key);
895
1421
  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);
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
+ }
899
1439
  case "Image": {
900
1440
  let src = rv(n.src);
901
1441
  if (typeof src !== "string" || !src) return null;
@@ -906,7 +1446,7 @@ function renderNode(node, ctx, key) {
906
1446
  if (typeof resolved === "string" && resolved.trim().toLowerCase().startsWith("javascript:")) return null;
907
1447
  const resolvedAlt = rv(n.alt);
908
1448
  const alt = typeof resolvedAlt === "string" ? resolvedAlt : void 0;
909
- 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);
910
1450
  }
911
1451
  case "Avatar": {
912
1452
  let src = rv(n.src);
@@ -918,7 +1458,7 @@ function renderNode(node, ctx, key) {
918
1458
  if (typeof resolved === "string" && resolved.trim().toLowerCase().startsWith("javascript:")) return null;
919
1459
  const resolvedSize = rv(n.size);
920
1460
  const size = typeof resolvedSize === "number" || typeof resolvedSize === "string" ? resolvedSize : void 0;
921
- 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);
922
1462
  }
923
1463
  case "Icon": {
924
1464
  const resolvedName = rv(n.name);
@@ -927,7 +1467,7 @@ function renderNode(node, ctx, key) {
927
1467
  const size = typeof resolvedSize === "number" || typeof resolvedSize === "string" ? resolvedSize : void 0;
928
1468
  const resolvedColor = rv(n.color);
929
1469
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
930
- return /* @__PURE__ */ jsx18(
1470
+ return /* @__PURE__ */ jsx20(
931
1471
  Icon,
932
1472
  {
933
1473
  name,
@@ -943,14 +1483,14 @@ function renderNode(node, ctx, key) {
943
1483
  case "Spacer": {
944
1484
  const resolvedSize = rv(n.size);
945
1485
  const size = typeof resolvedSize === "number" || typeof resolvedSize === "string" ? resolvedSize : void 0;
946
- return /* @__PURE__ */ jsx18(Spacer, { size, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1486
+ return /* @__PURE__ */ jsx20(Spacer, { size, style: cssStyle, hoverStyle: cssHoverStyle }, key);
947
1487
  }
948
1488
  case "Divider": {
949
1489
  const resolvedColor = rv(n.color);
950
1490
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
951
1491
  const resolvedThickness = rv(n.thickness);
952
1492
  const thickness = typeof resolvedThickness === "number" || typeof resolvedThickness === "string" ? resolvedThickness : void 0;
953
- return /* @__PURE__ */ jsx18(Divider, { color, thickness, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1493
+ return /* @__PURE__ */ jsx20(Divider, { color, thickness, style: cssStyle, hoverStyle: cssHoverStyle }, key);
954
1494
  }
955
1495
  case "ProgressBar": {
956
1496
  const resolvedValue = rv(n.value);
@@ -959,33 +1499,33 @@ function renderNode(node, ctx, key) {
959
1499
  const max = typeof resolvedMax === "number" ? resolvedMax : 100;
960
1500
  const resolvedColor = rv(n.color);
961
1501
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
962
- 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);
963
1503
  }
964
1504
  case "Badge": {
965
- const resolvedLabel = rv(n.label);
966
- const label = typeof resolvedLabel === "string" ? resolvedLabel : "";
1505
+ const label = resolveTextValue(n.label, ctx.state, ctx.locals);
967
1506
  const resolvedColor = rv(n.color);
968
1507
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
969
- return /* @__PURE__ */ jsx18(Badge, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1508
+ return /* @__PURE__ */ jsx20(Badge, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
970
1509
  }
971
1510
  case "Chip": {
972
- const resolvedLabel = rv(n.label);
973
- const label = typeof resolvedLabel === "string" ? resolvedLabel : "";
1511
+ const label = resolveTextValue(n.label, ctx.state, ctx.locals);
974
1512
  const resolvedColor = rv(n.color);
975
1513
  const color = typeof resolvedColor === "string" ? resolvedColor : void 0;
976
- return /* @__PURE__ */ jsx18(Chip, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
1514
+ return /* @__PURE__ */ jsx20(Chip, { label, color, style: cssStyle, hoverStyle: cssHoverStyle }, key);
977
1515
  }
978
1516
  case "Button": {
979
- const resolvedLabel = rv(n.label);
980
- const label = typeof resolvedLabel === "string" ? resolvedLabel : "";
1517
+ const label = resolveTextValue(n.label, ctx.state, ctx.locals);
981
1518
  const actionVal = n.action;
982
1519
  const action = typeof actionVal === "string" ? actionVal : "";
983
- return /* @__PURE__ */ jsx18(
1520
+ const resolvedDisabled = rv(n.disabled);
1521
+ const disabled = typeof resolvedDisabled === "boolean" ? resolvedDisabled : void 0;
1522
+ return /* @__PURE__ */ jsx20(
984
1523
  Button,
985
1524
  {
986
1525
  label,
987
1526
  action,
988
1527
  onAction: ctx.onAction,
1528
+ disabled,
989
1529
  style: cssStyle,
990
1530
  hoverStyle: cssHoverStyle
991
1531
  },
@@ -997,12 +1537,47 @@ function renderNode(node, ctx, key) {
997
1537
  const value = typeof resolvedValue === "boolean" ? resolvedValue : false;
998
1538
  const onToggleVal = n.onToggle;
999
1539
  const onToggle = typeof onToggleVal === "string" ? onToggleVal : "";
1000
- return /* @__PURE__ */ jsx18(
1540
+ const resolvedDisabled = rv(n.disabled);
1541
+ const disabled = typeof resolvedDisabled === "boolean" ? resolvedDisabled : void 0;
1542
+ return /* @__PURE__ */ jsx20(
1001
1543
  Toggle,
1002
1544
  {
1003
1545
  value,
1004
1546
  onToggle,
1005
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,
1006
1581
  style: cssStyle,
1007
1582
  hoverStyle: cssHoverStyle
1008
1583
  },
@@ -1050,7 +1625,7 @@ function renderForLoop(loop, ctx) {
1050
1625
  }
1051
1626
  return elements;
1052
1627
  }
1053
- function renderTree(rootNode, state, assets, cardStyles, iconResolver, onAction, onError, responsive = { compact: false }) {
1628
+ function renderTree(rootNode, state, assets, cardStyles, iconResolver, onAction, onError, responsive = { compact: false }, fragments) {
1054
1629
  const limits = {
1055
1630
  nodeCount: 0,
1056
1631
  textBytes: 0,
@@ -1061,6 +1636,8 @@ function renderTree(rootNode, state, assets, cardStyles, iconResolver, onAction,
1061
1636
  state,
1062
1637
  assets,
1063
1638
  cardStyles,
1639
+ fragments,
1640
+ fragmentStack: [],
1064
1641
  iconResolver,
1065
1642
  onAction,
1066
1643
  onError,
@@ -1071,7 +1648,7 @@ function renderTree(rootNode, state, assets, cardStyles, iconResolver, onAction,
1071
1648
  }
1072
1649
 
1073
1650
  // src/UGCRenderer.tsx
1074
- import { jsx as jsx19 } from "react/jsx-runtime";
1651
+ import { jsx as jsx21 } from "react/jsx-runtime";
1075
1652
  function UGCRenderer({
1076
1653
  card,
1077
1654
  viewName,
@@ -1082,8 +1659,8 @@ function UGCRenderer({
1082
1659
  iconResolver,
1083
1660
  onAction
1084
1661
  }) {
1085
- const [containerElement, setContainerElement] = useState2(null);
1086
- const [containerWidth, setContainerWidth] = useState2(
1662
+ const [containerElement, setContainerElement] = useState4(null);
1663
+ const [containerWidth, setContainerWidth] = useState4(
1087
1664
  typeof window === "undefined" ? null : window.innerWidth
1088
1665
  );
1089
1666
  const result = useMemo2(() => {
@@ -1103,14 +1680,16 @@ function UGCRenderer({
1103
1680
  ...stateOverride ?? {}
1104
1681
  };
1105
1682
  const cardStyles = cardObj.styles;
1683
+ const fragments = cardObj.fragments;
1106
1684
  return {
1107
1685
  valid: true,
1108
1686
  rootNode: views[selectedView],
1109
1687
  state: mergedState,
1110
- cardStyles
1688
+ cardStyles,
1689
+ fragments
1111
1690
  };
1112
1691
  }, [card, viewName, stateOverride]);
1113
- useEffect(() => {
1692
+ useEffect2(() => {
1114
1693
  if (!containerElement) {
1115
1694
  return;
1116
1695
  }
@@ -1146,7 +1725,7 @@ function UGCRenderer({
1146
1725
  }
1147
1726
  return null;
1148
1727
  }
1149
- return /* @__PURE__ */ jsx19(UGCContainer, { ref: setContainerElement, style: containerStyle2, children: renderTree(
1728
+ return /* @__PURE__ */ jsx21(UGCContainer, { ref: setContainerElement, style: containerStyle2, children: renderTree(
1150
1729
  result.rootNode,
1151
1730
  result.state,
1152
1731
  assets,
@@ -1154,10 +1733,12 @@ function UGCRenderer({
1154
1733
  iconResolver,
1155
1734
  onAction,
1156
1735
  onError,
1157
- responsive
1736
+ responsive,
1737
+ result.fragments
1158
1738
  ) });
1159
1739
  }
1160
1740
  export {
1741
+ Accordion,
1161
1742
  Avatar,
1162
1743
  Badge,
1163
1744
  Box,
@@ -1172,6 +1753,7 @@ export {
1172
1753
  Row,
1173
1754
  Spacer,
1174
1755
  Stack,
1756
+ Tabs,
1175
1757
  Text,
1176
1758
  Toggle,
1177
1759
  UGCContainer,