@almadar/ui 5.157.0 → 5.159.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.
@@ -15,7 +15,7 @@ import { useTranslate } from '@almadar/ui/hooks';
15
15
  import { useUISlots, ThemeProvider, useTheme } from '@almadar/ui/context';
16
16
  export { DesignThemeProvider, useDesignTheme } from '@almadar/ui/context';
17
17
  import { evaluate, createMinimalContext } from '@almadar/evaluator';
18
- import { wrapCallbackForEvent } from '@almadar/runtime/ui';
18
+ import { wrapCallbackForEvent, perfStart, perfEnd } from '@almadar/runtime/ui';
19
19
  import { useLocation, useNavigate, Link, Outlet } from 'react-router-dom';
20
20
  import ELK from 'elkjs/lib/elk.bundled.js';
21
21
  import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light.js';
@@ -286,11 +286,54 @@ function isPlainObject(value) {
286
286
  if (typeof value === "function") return false;
287
287
  return true;
288
288
  }
289
+ function isEvaluatorResolvedData(value) {
290
+ return evaluatorResolvedData.has(value);
291
+ }
292
+ function brandResolved(value) {
293
+ if (value !== null && typeof value === "object" && !React87__default.isValidElement(value) && !(value instanceof Date)) {
294
+ resolvedMarkerFree.add(value);
295
+ }
296
+ }
297
+ function subtreeHasMarker(value) {
298
+ if (resolvedMarkerFree.has(value)) return false;
299
+ const cached = markerPresenceCache.get(value);
300
+ if (cached !== void 0) return cached;
301
+ let found = false;
302
+ const children = Array.isArray(value) ? value : Object.values(value);
303
+ for (const child of children) {
304
+ if (isRenderBindingMarker(child)) {
305
+ found = true;
306
+ break;
307
+ }
308
+ if (Array.isArray(child) || isPlainObject(child)) {
309
+ if (subtreeHasMarker(child)) {
310
+ found = true;
311
+ break;
312
+ }
313
+ }
314
+ }
315
+ markerPresenceCache.set(value, found);
316
+ return found;
317
+ }
289
318
  function walkValue(value, scopeTrait, entity, config, state) {
290
319
  if (isRenderBindingMarker(value)) {
291
- return { resolved: resolveMarkerExpression(value.expression, entity, config, state), changed: true };
320
+ const cached = markerResolutionCache.get(value);
321
+ if (cached !== void 0 && cached.entity === entity && cached.config === config && cached.state === state) {
322
+ return { resolved: cached.resolved, changed: false };
323
+ }
324
+ const resolved = resolveMarkerExpression(value.expression, entity, config, state);
325
+ markerResolutionCache.set(value, { entity, config, state, resolved });
326
+ if (resolved !== null && typeof resolved === "object" && !React87__default.isValidElement(resolved) && !(resolved instanceof Date)) {
327
+ resolvedMarkerFree.add(resolved);
328
+ evaluatorResolvedData.add(resolved);
329
+ }
330
+ return { resolved, changed: true };
292
331
  }
293
332
  if (Array.isArray(value)) {
333
+ if (!subtreeHasMarker(value)) {
334
+ brandResolved(value);
335
+ return { resolved: value, changed: false };
336
+ }
294
337
  const out = [];
295
338
  let changed = false;
296
339
  for (const item of value) {
@@ -305,9 +348,14 @@ function walkValue(value, scopeTrait, entity, config, state) {
305
348
  out.push(resolved);
306
349
  if (itemChanged) changed = true;
307
350
  }
351
+ brandResolved(out);
308
352
  return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
309
353
  }
310
354
  if (isPlainObject(value)) {
355
+ if (!subtreeHasMarker(value)) {
356
+ brandResolved(value);
357
+ return { resolved: value, changed: false };
358
+ }
311
359
  const sourceTrait = value._sourceTrait;
312
360
  if (typeof sourceTrait === "string" && sourceTrait !== scopeTrait) {
313
361
  return { resolved: value, changed: false };
@@ -319,11 +367,13 @@ function walkValue(value, scopeTrait, entity, config, state) {
319
367
  out[key] = resolved;
320
368
  if (itemChanged) changed = true;
321
369
  }
370
+ brandResolved(out);
322
371
  return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
323
372
  }
324
373
  return { resolved: value, changed: false };
325
374
  }
326
375
  function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
376
+ if (resolvedMarkerFree.has(props)) return props;
327
377
  const out = {};
328
378
  let changed = false;
329
379
  for (const [key, value] of Object.entries(props)) {
@@ -331,11 +381,17 @@ function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
331
381
  out[key] = resolved;
332
382
  if (propChanged) changed = true;
333
383
  }
384
+ brandResolved(out);
334
385
  return changed ? out : props;
335
386
  }
387
+ var markerPresenceCache, markerResolutionCache, resolvedMarkerFree, evaluatorResolvedData;
336
388
  var init_resolve_render_bindings = __esm({
337
389
  "lib/resolve-render-bindings.ts"() {
338
390
  "use client";
391
+ markerPresenceCache = /* @__PURE__ */ new WeakMap();
392
+ markerResolutionCache = /* @__PURE__ */ new WeakMap();
393
+ resolvedMarkerFree = /* @__PURE__ */ new WeakSet();
394
+ evaluatorResolvedData = /* @__PURE__ */ new WeakSet();
339
395
  }
340
396
  });
341
397
  function cn(...inputs) {
@@ -11769,6 +11825,10 @@ var init_molecules = __esm({
11769
11825
  init_puzzleObject();
11770
11826
  }
11771
11827
  });
11828
+ var init_perf = __esm({
11829
+ "lib/perf.ts"() {
11830
+ }
11831
+ });
11772
11832
  function themeBodyFont(el) {
11773
11833
  if (typeof getComputedStyle !== "function") return "system-ui, sans-serif";
11774
11834
  const v = getComputedStyle(el).getPropertyValue("--font-family-body").trim();
@@ -12117,6 +12177,7 @@ var init_LearningCanvas = __esm({
12117
12177
  "components/learning/atoms/LearningCanvas.tsx"() {
12118
12178
  "use client";
12119
12179
  init_cn();
12180
+ init_perf();
12120
12181
  init_useEventBus();
12121
12182
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
12122
12183
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
@@ -12160,6 +12221,7 @@ var init_LearningCanvas = __esm({
12160
12221
  return [...shapes, ...traceOut, ...readoutOut];
12161
12222
  }, [shapes, traces, readouts, width, height]);
12162
12223
  const draw = useCallback(() => {
12224
+ const _perfT = perfStart("learningcanvas:paint");
12163
12225
  const canvas = canvasRef.current;
12164
12226
  if (!canvas) return;
12165
12227
  const ctx = canvas.getContext("2d");
@@ -12181,6 +12243,7 @@ var init_LearningCanvas = __esm({
12181
12243
  for (const shape of derivedShapes) {
12182
12244
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
12183
12245
  }
12246
+ perfEnd("learningcanvas:paint", _perfT);
12184
12247
  }, [width, height, backgroundColor, derivedShapes]);
12185
12248
  useEffect(() => {
12186
12249
  draw();
@@ -27368,7 +27431,7 @@ function fileIcon(name) {
27368
27431
  return "file";
27369
27432
  }
27370
27433
  }
27371
- var TreeNodeItem, FileTree;
27434
+ var TreeNodeItem, FlatTreeNodeItem, FileTree;
27372
27435
  var init_FileTree = __esm({
27373
27436
  "components/core/molecules/FileTree.tsx"() {
27374
27437
  "use client";
@@ -27453,14 +27516,101 @@ var init_FileTree = __esm({
27453
27516
  )) })
27454
27517
  ] });
27455
27518
  };
27519
+ FlatTreeNodeItem = ({
27520
+ item,
27521
+ depth,
27522
+ indent,
27523
+ childrenByParent,
27524
+ onNodeSelect,
27525
+ defaultExpanded = false
27526
+ }) => {
27527
+ const [expanded, setExpanded] = useState(defaultExpanded || depth < 1);
27528
+ const children = childrenByParent.get(item.id);
27529
+ const hasChildren = !!children && children.length > 0;
27530
+ const handleClick = useCallback(() => {
27531
+ if (hasChildren) setExpanded((prev) => !prev);
27532
+ onNodeSelect?.(item.id);
27533
+ }, [hasChildren, item.id, onNodeSelect]);
27534
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
27535
+ /* @__PURE__ */ jsxs(
27536
+ Box,
27537
+ {
27538
+ className: "flex items-center gap-1.5 py-0.5 px-2 cursor-pointer rounded-sm transition-colors hover:bg-muted",
27539
+ style: { paddingLeft: depth * indent + 8 },
27540
+ onClick: handleClick,
27541
+ role: "treeitem",
27542
+ "aria-expanded": hasChildren ? expanded : void 0,
27543
+ children: [
27544
+ hasChildren ? /* @__PURE__ */ jsx(
27545
+ Icon,
27546
+ {
27547
+ name: expanded ? "chevron-down" : "chevron-right",
27548
+ size: "xs",
27549
+ className: "text-[var(--color-muted-foreground)] flex-shrink-0"
27550
+ }
27551
+ ) : /* @__PURE__ */ jsx(Box, { style: { width: 12, flexShrink: 0 } }),
27552
+ /* @__PURE__ */ jsx(
27553
+ Icon,
27554
+ {
27555
+ name: item.icon ?? (hasChildren ? expanded ? "folder-open" : "folder" : "file"),
27556
+ size: "xs",
27557
+ className: hasChildren ? "text-[var(--color-warning)]" : "text-[var(--color-muted-foreground)]"
27558
+ }
27559
+ ),
27560
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "truncate font-mono text-xs", children: item.label })
27561
+ ]
27562
+ }
27563
+ ),
27564
+ hasChildren && expanded && /* @__PURE__ */ jsx(Box, { role: "group", children: children.map((child) => /* @__PURE__ */ jsx(
27565
+ FlatTreeNodeItem,
27566
+ {
27567
+ item: child,
27568
+ depth: depth + 1,
27569
+ indent,
27570
+ childrenByParent,
27571
+ onNodeSelect
27572
+ },
27573
+ child.id
27574
+ )) })
27575
+ ] });
27576
+ };
27456
27577
  FileTree = ({
27457
27578
  tree,
27579
+ items,
27458
27580
  selectedPath,
27459
27581
  onFileSelect,
27582
+ onNodeSelect,
27460
27583
  className,
27461
27584
  indent = 16
27462
27585
  }) => {
27463
- if (tree.length === 0) return null;
27586
+ if (items) {
27587
+ if (items.length === 0) return null;
27588
+ const ids = new Set(items.map((node) => node.id));
27589
+ const childrenByParent = /* @__PURE__ */ new Map();
27590
+ const roots = [];
27591
+ for (const item of items) {
27592
+ if (item.parentId && ids.has(item.parentId)) {
27593
+ const siblings = childrenByParent.get(item.parentId);
27594
+ if (siblings) siblings.push(item);
27595
+ else childrenByParent.set(item.parentId, [item]);
27596
+ } else {
27597
+ roots.push(item);
27598
+ }
27599
+ }
27600
+ return /* @__PURE__ */ jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsx(
27601
+ FlatTreeNodeItem,
27602
+ {
27603
+ item,
27604
+ depth: 0,
27605
+ indent,
27606
+ childrenByParent,
27607
+ onNodeSelect,
27608
+ defaultExpanded: true
27609
+ },
27610
+ item.id
27611
+ )) });
27612
+ }
27613
+ if (!tree || tree.length === 0) return null;
27464
27614
  return /* @__PURE__ */ jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: tree.map((node) => /* @__PURE__ */ jsx(
27465
27615
  TreeNodeItem,
27466
27616
  {
@@ -28855,7 +29005,7 @@ var init_debug = __esm({
28855
29005
  createLogger("almadar:ui:debug:game-state");
28856
29006
  }
28857
29007
  });
28858
- var isRelationsDebugEnabled, RelationSelect;
29008
+ var isRelationsDebugEnabled, MANY_CARDINALITIES, RelationSelect;
28859
29009
  var init_RelationSelect = __esm({
28860
29010
  "components/core/molecules/RelationSelect.tsx"() {
28861
29011
  "use client";
@@ -28869,6 +29019,11 @@ var init_RelationSelect = __esm({
28869
29019
  init_Typography();
28870
29020
  init_debug();
28871
29021
  isRelationsDebugEnabled = () => isDebugEnabled();
29022
+ MANY_CARDINALITIES = [
29023
+ "many",
29024
+ "one-to-many",
29025
+ "many-to-many"
29026
+ ];
28872
29027
  RelationSelect = ({
28873
29028
  value,
28874
29029
  onChange,
@@ -30493,6 +30648,7 @@ var init_MathCanvas = __esm({
30493
30648
  "components/learning/molecules/MathCanvas.tsx"() {
30494
30649
  "use client";
30495
30650
  init_useEventBus();
30651
+ init_perf();
30496
30652
  init_atoms();
30497
30653
  init_Stack();
30498
30654
  init_LearningCanvas();
@@ -30533,17 +30689,21 @@ var init_MathCanvas = __esm({
30533
30689
  error
30534
30690
  }) => {
30535
30691
  const eventBus = useEventBus();
30692
+ const keyMapKey = keyMap ? JSON.stringify(keyMap) : null;
30693
+ const keyUpMapKey = keyUpMap ? JSON.stringify(keyUpMap) : null;
30694
+ const stableKeyMap = useMemo(() => keyMap, [keyMapKey]);
30695
+ const stableKeyUpMap = useMemo(() => keyUpMap, [keyUpMapKey]);
30536
30696
  useEffect(() => {
30537
- if (!keyMap && !keyUpMap) return;
30697
+ if (!stableKeyMap && !stableKeyUpMap) return;
30538
30698
  const onDown = (e) => {
30539
- const ev = keyMap?.[e.code];
30699
+ const ev = stableKeyMap?.[e.code];
30540
30700
  if (ev) {
30541
30701
  eventBus.emit(`UI:${ev}`, {});
30542
30702
  e.preventDefault();
30543
30703
  }
30544
30704
  };
30545
30705
  const onUp = (e) => {
30546
- const ev = keyUpMap?.[e.code];
30706
+ const ev = stableKeyUpMap?.[e.code];
30547
30707
  if (ev) eventBus.emit(`UI:${ev}`, {});
30548
30708
  };
30549
30709
  window.addEventListener("keydown", onDown);
@@ -30552,8 +30712,9 @@ var init_MathCanvas = __esm({
30552
30712
  window.removeEventListener("keydown", onDown);
30553
30713
  window.removeEventListener("keyup", onUp);
30554
30714
  };
30555
- }, [keyMap, keyUpMap, eventBus]);
30715
+ }, [stableKeyMap, stableKeyUpMap, eventBus]);
30556
30716
  const derivedShapes = useMemo(() => {
30717
+ const _perfT = perfStart("mathcanvas:derive");
30557
30718
  const out = [];
30558
30719
  const margin = 24;
30559
30720
  const plotW = width - margin * 2;
@@ -30797,6 +30958,7 @@ var init_MathCanvas = __esm({
30797
30958
  }
30798
30959
  }
30799
30960
  out.push(...shapes);
30961
+ perfEnd("mathcanvas:derive", _perfT);
30800
30962
  return out;
30801
30963
  }, [
30802
30964
  width,
@@ -32060,12 +32222,12 @@ var init_MapView = __esm({
32060
32222
  shadowSize: [41, 41]
32061
32223
  });
32062
32224
  L.Marker.prototype.options.icon = defaultIcon;
32063
- const { useEffect: useEffect67, useRef: useRef64, useCallback: useCallback97, useState: useState95 } = React87__default;
32225
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback97, useState: useState95 } = React87__default;
32064
32226
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
32065
32227
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
32066
32228
  function MapUpdater({ centerLat, centerLng, zoom }) {
32067
32229
  const map = useMap();
32068
- const prevRef = useRef64({ centerLat, centerLng, zoom });
32230
+ const prevRef = useRef65({ centerLat, centerLng, zoom });
32069
32231
  useEffect67(() => {
32070
32232
  const prev = prevRef.current;
32071
32233
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -37150,7 +37312,7 @@ var init_RichBlockEditor = __esm({
37150
37312
  {
37151
37313
  variant: "bordered",
37152
37314
  padding: "none",
37153
- className: cn("flex flex-col", className),
37315
+ className: cn("flex flex-col text-card-foreground", className),
37154
37316
  children: [
37155
37317
  enableBlocks && showToolbar && !readOnly && /* @__PURE__ */ jsx(
37156
37318
  Box,
@@ -42378,6 +42540,7 @@ var init_DetailPanel = __esm({
42378
42540
  "use client";
42379
42541
  init_atoms();
42380
42542
  init_Box();
42543
+ init_Input();
42381
42544
  init_Stack();
42382
42545
  init_SimpleGrid();
42383
42546
  init_Menu();
@@ -42393,6 +42556,7 @@ var init_DetailPanel = __esm({
42393
42556
  ReactMarkdown2 = lazy(() => import('react-markdown'));
42394
42557
  DetailPanel = ({
42395
42558
  title: propTitle,
42559
+ onTitleCommit,
42396
42560
  subtitle,
42397
42561
  status,
42398
42562
  avatar,
@@ -42414,6 +42578,7 @@ var init_DetailPanel = __esm({
42414
42578
  }) => {
42415
42579
  const eventBus = useEventBus();
42416
42580
  const { t } = useTranslate();
42581
+ const [titleDraft, setTitleDraft] = React87__default.useState(null);
42417
42582
  const isFieldDefArray = (arr) => {
42418
42583
  if (!arr || arr.length === 0) return false;
42419
42584
  const first = arr[0];
@@ -42634,6 +42799,50 @@ var init_DetailPanel = __esm({
42634
42799
  }),
42635
42800
  status && /* @__PURE__ */ jsx(Badge, { variant: status.variant ?? "default", children: status.label })
42636
42801
  ] });
42802
+ const commitTitle = () => {
42803
+ if (!onTitleCommit) return;
42804
+ const next = (titleDraft ?? "").trim();
42805
+ setTitleDraft(null);
42806
+ if (!next || next === title) return;
42807
+ onTitleCommit(next, normalizedData?.id !== void 0 ? String(normalizedData.id) : "");
42808
+ };
42809
+ const titleNode = onTitleCommit && titleDraft !== null ? /* @__PURE__ */ jsx(
42810
+ Input,
42811
+ {
42812
+ value: titleDraft,
42813
+ autoFocus: true,
42814
+ "aria-label": t("common.title"),
42815
+ className: "h-auto py-1 text-3xl font-bold tracking-tight",
42816
+ onChange: (e) => setTitleDraft(e.target.value),
42817
+ onBlur: commitTitle,
42818
+ onKeyDown: (e) => {
42819
+ if (e.key === "Enter") {
42820
+ e.preventDefault();
42821
+ commitTitle();
42822
+ } else if (e.key === "Escape") {
42823
+ e.preventDefault();
42824
+ setTitleDraft(null);
42825
+ }
42826
+ },
42827
+ "data-testid": "detail-title-input"
42828
+ }
42829
+ ) : onTitleCommit ? /* @__PURE__ */ jsx(
42830
+ Box,
42831
+ {
42832
+ role: "button",
42833
+ tabIndex: 0,
42834
+ className: "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
42835
+ onClick: () => setTitleDraft(title ?? ""),
42836
+ onKeyDown: (e) => {
42837
+ if (e.key === "Enter" || e.key === " ") {
42838
+ e.preventDefault();
42839
+ setTitleDraft(title ?? "");
42840
+ }
42841
+ },
42842
+ "data-testid": "detail-title-editable",
42843
+ children: /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" })
42844
+ }
42845
+ ) : /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" });
42637
42846
  const content = /* @__PURE__ */ jsx(Card, { variant: "elevated", children: /* @__PURE__ */ jsxs(VStack, { gap: "md", className: "p-6", children: [
42638
42847
  /* @__PURE__ */ jsxs(HStack, { justify: "between", align: "start", gap: "md", children: [
42639
42848
  /* @__PURE__ */ jsxs(HStack, { align: "start", gap: "sm", className: "min-w-0", children: [
@@ -42653,7 +42862,7 @@ var init_DetailPanel = __esm({
42653
42862
  avatar,
42654
42863
  /* @__PURE__ */ jsxs(VStack, { gap: "xs", className: "min-w-0", children: [
42655
42864
  /* @__PURE__ */ jsxs(HStack, { align: "center", gap: "sm", wrap: true, children: [
42656
- /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" }),
42865
+ titleNode,
42657
42866
  statusBadges
42658
42867
  ] }),
42659
42868
  subtitle && /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: subtitle })
@@ -42989,6 +43198,9 @@ function determineInputType(field) {
42989
43198
  if (field.type === "relation" || field.relation) {
42990
43199
  return "relation";
42991
43200
  }
43201
+ if (field.type === "array") {
43202
+ return "array";
43203
+ }
42992
43204
  if (field.type === "enum" || field.values || getEnumOptions(field).length > 0) {
42993
43205
  return "select";
42994
43206
  }
@@ -43064,6 +43276,7 @@ var init_Form = __esm({
43064
43276
  init_Typography();
43065
43277
  init_Icon();
43066
43278
  init_RelationSelect();
43279
+ init_TagInput();
43067
43280
  init_UploadDropZone();
43068
43281
  init_Alert();
43069
43282
  init_useEventBus();
@@ -43143,7 +43356,7 @@ var init_Form = __esm({
43143
43356
  values: "values" in f3 ? f3.values : void 0,
43144
43357
  min: f3.min,
43145
43358
  max: f3.max,
43146
- relation: "relation" in f3 ? { entity: f3.relation.entity } : void 0
43359
+ relation: "relation" in f3 ? { entity: f3.relation.entity, cardinality: f3.relation.cardinality } : void 0
43147
43360
  })
43148
43361
  );
43149
43362
  }, [entity, fields]);
@@ -43210,7 +43423,7 @@ var init_Form = __esm({
43210
43423
  });
43211
43424
  debug(
43212
43425
  "forms",
43213
- `Calculation triggered: ${calc.variableName} = ${value}`
43426
+ `Calculation triggered: ${calc.variableName} = ${String(value)}`
43214
43427
  );
43215
43428
  }
43216
43429
  });
@@ -43378,7 +43591,7 @@ var init_Form = __esm({
43378
43591
  values: "values" in entityField ? entityField.values : void 0,
43379
43592
  min: entityField.min,
43380
43593
  max: entityField.max,
43381
- relation: "relation" in entityField ? { entity: entityField.relation.entity } : void 0
43594
+ relation: "relation" in entityField ? { entity: entityField.relation.entity, cardinality: entityField.relation.cardinality } : void 0
43382
43595
  };
43383
43596
  }
43384
43597
  return { name: field, type: "string" };
@@ -43490,6 +43703,22 @@ var init_Form = __esm({
43490
43703
  case "relation": {
43491
43704
  const relationOptions = relationsData[fieldName] || [];
43492
43705
  const relationLoading = relationsLoading[fieldName] || false;
43706
+ if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
43707
+ const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
43708
+ return /* @__PURE__ */ jsx(
43709
+ Select,
43710
+ {
43711
+ ...commonProps,
43712
+ multiple: true,
43713
+ searchable: true,
43714
+ clearable: true,
43715
+ options: [...relationOptions],
43716
+ value: selectedValues,
43717
+ onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
43718
+ placeholder: field.placeholder || `Select ${label}...`
43719
+ }
43720
+ );
43721
+ }
43493
43722
  return /* @__PURE__ */ jsx(
43494
43723
  RelationSelect,
43495
43724
  {
@@ -43504,6 +43733,18 @@ var init_Form = __esm({
43504
43733
  }
43505
43734
  );
43506
43735
  }
43736
+ case "array": {
43737
+ const arrayValue = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : currentValue != null && currentValue !== "" ? [String(currentValue)] : [];
43738
+ return /* @__PURE__ */ jsx(
43739
+ TagInput,
43740
+ {
43741
+ placeholder: field.placeholder,
43742
+ disabled: isLoading,
43743
+ value: arrayValue,
43744
+ onChange: (next) => handleChange(fieldName, [...next])
43745
+ }
43746
+ );
43747
+ }
43507
43748
  case "number":
43508
43749
  return /* @__PURE__ */ jsx(
43509
43750
  Input,
@@ -49053,7 +49294,10 @@ function enrichFormFields(fields, entityDef) {
49053
49294
  enriched.values = entityField.enumValues;
49054
49295
  }
49055
49296
  if (entityField.relation) {
49056
- enriched.relation = entityField.relation.entity;
49297
+ enriched.relation = {
49298
+ entity: entityField.relation.entity,
49299
+ cardinality: entityField.relation.cardinality
49300
+ };
49057
49301
  }
49058
49302
  return enriched;
49059
49303
  }
@@ -49081,7 +49325,10 @@ function enrichFormFields(fields, entityDef) {
49081
49325
  }
49082
49326
  }
49083
49327
  if (!obj.relation && entityField.relation) {
49084
- enriched.relation = entityField.relation.entity;
49328
+ enriched.relation = {
49329
+ entity: entityField.relation.entity,
49330
+ cardinality: entityField.relation.cardinality
49331
+ };
49085
49332
  }
49086
49333
  return enriched;
49087
49334
  }
@@ -49096,7 +49343,12 @@ function enrichDetailFields(fields, entityDef) {
49096
49343
  const meta = { type: entityField.type };
49097
49344
  const values = entityField.values ?? entityField.enumValues;
49098
49345
  if (values && values.length > 0) meta.values = values;
49099
- if (entityField.relation) meta.relation = entityField.relation.entity;
49346
+ if (entityField.relation) {
49347
+ meta.relation = {
49348
+ entity: entityField.relation.entity,
49349
+ cardinality: entityField.relation.cardinality
49350
+ };
49351
+ }
49100
49352
  return meta;
49101
49353
  };
49102
49354
  return fields.map((field) => {
@@ -49650,6 +49902,33 @@ function isPlainConfigObject(value) {
49650
49902
  const proto = Object.getPrototypeOf(value);
49651
49903
  return proto === Object.prototype || proto === null;
49652
49904
  }
49905
+ function subtreeHasTraitRef(value) {
49906
+ if (isEvaluatorResolvedData(value)) return false;
49907
+ const cached = traitRefPresenceCache.get(value);
49908
+ if (cached !== void 0) return cached;
49909
+ let found = false;
49910
+ const children = Array.isArray(value) ? value : Object.values(value);
49911
+ for (const child of children) {
49912
+ if (typeof child === "string" && TRAIT_BINDING_RE.test(child)) {
49913
+ found = true;
49914
+ break;
49915
+ }
49916
+ if (isRenderBindingMarker(child)) continue;
49917
+ if (Array.isArray(child)) {
49918
+ if (subtreeHasTraitRef(child)) {
49919
+ found = true;
49920
+ break;
49921
+ }
49922
+ } else if (child !== null && typeof child === "object" && isPlainConfigObject(child)) {
49923
+ if (subtreeHasTraitRef(child)) {
49924
+ found = true;
49925
+ break;
49926
+ }
49927
+ }
49928
+ }
49929
+ traitRefPresenceCache.set(value, found);
49930
+ return found;
49931
+ }
49653
49932
  function substituteTraitRefsDeep(value, pathKey) {
49654
49933
  if (isRenderBindingMarker(value)) return value;
49655
49934
  if (typeof value === "string") {
@@ -49664,11 +49943,13 @@ function substituteTraitRefsDeep(value, pathKey) {
49664
49943
  return value;
49665
49944
  }
49666
49945
  if (Array.isArray(value)) {
49946
+ if (!subtreeHasTraitRef(value)) return value;
49667
49947
  return value.map(
49668
49948
  (item, i) => substituteTraitRefsDeep(item, `${pathKey}[${i}]`)
49669
49949
  );
49670
49950
  }
49671
49951
  if (typeof value === "object" && isPlainConfigObject(value)) {
49952
+ if (!subtreeHasTraitRef(value)) return value;
49672
49953
  const out = {};
49673
49954
  for (const [k, v] of Object.entries(value)) {
49674
49955
  out[k] = substituteTraitRefsDeep(v, `${pathKey}.${k}`);
@@ -49695,6 +49976,10 @@ function renderPatternProps(props, onDismiss, propsSchema) {
49695
49976
  };
49696
49977
  rendered[key] = /* @__PURE__ */ jsx(SlotContentRenderer, { content: childContent, onDismiss });
49697
49978
  } else if (Array.isArray(value)) {
49979
+ if (!isEvaluatorResolvedData(value) && value.some((el) => isPatternConfig(el))) ; else if (!subtreeHasTraitRef(value)) {
49980
+ rendered[key] = value;
49981
+ continue;
49982
+ }
49698
49983
  const isDataArray = propsSchema?.[key]?.items?.types?.includes("object") ?? false;
49699
49984
  rendered[key] = value.map((item, i) => {
49700
49985
  const el = item;
@@ -49957,7 +50242,7 @@ function UISlotRenderer({
49957
50242
  }
49958
50243
  return wrapped;
49959
50244
  }
49960
- var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext, SLOT_SKELETON_MAP, SELF_OVERLAY_PATTERNS, CONTENT_NODE_SLOTS, PATTERNS_WITH_CHILDREN;
50245
+ var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext, SLOT_SKELETON_MAP, SELF_OVERLAY_PATTERNS, CONTENT_NODE_SLOTS, PATTERNS_WITH_CHILDREN, traitRefPresenceCache;
49961
50246
  var init_UISlotRenderer = __esm({
49962
50247
  "components/core/organisms/UISlotRenderer.tsx"() {
49963
50248
  "use client";
@@ -50031,6 +50316,7 @@ var init_UISlotRenderer = __esm({
50031
50316
  "alert",
50032
50317
  "dialog"
50033
50318
  ]);
50319
+ traitRefPresenceCache = /* @__PURE__ */ new WeakMap();
50034
50320
  UISlotRenderer.displayName = "UISlotRenderer";
50035
50321
  }
50036
50322
  });
@@ -51135,6 +51421,53 @@ function useNavigationId2() {
51135
51421
 
51136
51422
  // providers/ServerBridge.tsx
51137
51423
  init_useEventBus();
51424
+
51425
+ // lib/tick-send-relay.ts
51426
+ function createTickSendRelay(send) {
51427
+ const lanes = /* @__PURE__ */ new Map();
51428
+ const flush = (key, lane) => {
51429
+ const next = lane.pending;
51430
+ lane.pending = void 0;
51431
+ if (next === void 0) {
51432
+ lane.inFlight = false;
51433
+ return;
51434
+ }
51435
+ lane.inFlight = true;
51436
+ void send(key, next).catch(() => void 0).then(() => flush(key, lane));
51437
+ };
51438
+ return {
51439
+ send(key, value) {
51440
+ const lane = lanes.get(key) ?? { inFlight: false };
51441
+ lanes.set(key, lane);
51442
+ if (lane.inFlight) {
51443
+ lane.pending = value;
51444
+ return;
51445
+ }
51446
+ lane.pending = value;
51447
+ flush(key, lane);
51448
+ },
51449
+ clear() {
51450
+ for (const lane of lanes.values()) {
51451
+ lane.pending = void 0;
51452
+ }
51453
+ }
51454
+ };
51455
+ }
51456
+
51457
+ // lib/command-send-pump.ts
51458
+ function createCommandSendPump() {
51459
+ let tail = Promise.resolve();
51460
+ return {
51461
+ enqueue(job) {
51462
+ const result = tail.then(job);
51463
+ tail = result.then(
51464
+ () => void 0,
51465
+ () => void 0
51466
+ );
51467
+ return result;
51468
+ }
51469
+ };
51470
+ }
51138
51471
  var xOrbitalLog = createLogger("almadar:runtime:cross-orbital");
51139
51472
  var serverBridgeLog = createLogger("almadar:ui:server-bridge");
51140
51473
  function reEmitServerEvent(eventBus, emitted, origin) {
@@ -51223,8 +51556,8 @@ function createHttpTransport(serverUrl) {
51223
51556
  } catch {
51224
51557
  }
51225
51558
  },
51226
- sendEvent: async (orbitalName, event, payload, clientId) => {
51227
- const body = { event, payload, clientId };
51559
+ sendEvent: async (orbitalName, event, payload, clientId, tick, sourceTrait) => {
51560
+ const body = { event, payload, clientId, tick, sourceTrait };
51228
51561
  const res = await fetch(`${serverUrl}/${orbitalName}/events`, {
51229
51562
  method: "POST",
51230
51563
  headers: { "Content-Type": "application/json" },
@@ -51269,96 +51602,118 @@ function ServerBridgeProvider({
51269
51602
  async () => transport.unregister(),
51270
51603
  [transport]
51271
51604
  );
51272
- const sendEvent = useCallback(async (orbitalName, event, payload) => {
51605
+ const tickRelay = useMemo(
51606
+ () => createTickSendRelay(async (_key, snap) => {
51607
+ await transport.sendEvent(snap.orbitalName, snap.event, snap.payload, getTabClientId(), snap.tick, snap.sourceTrait);
51608
+ }),
51609
+ [transport]
51610
+ );
51611
+ useEffect(() => () => tickRelay.clear(), [tickRelay]);
51612
+ const commandPump = useMemo(createCommandSendPump, []);
51613
+ const disposedRef = useRef(false);
51614
+ useEffect(() => {
51615
+ disposedRef.current = false;
51616
+ return () => {
51617
+ disposedRef.current = true;
51618
+ };
51619
+ }, []);
51620
+ const sendEvent = useCallback(async (orbitalName, event, payload, tick, sourceTrait) => {
51273
51621
  const emptyMeta = { success: false, clientEffects: 0, dataEntities: {}, emittedEvents: [] };
51274
51622
  if (!connected) return { effects: [], meta: emptyMeta };
51275
- try {
51276
- const result = await transport.sendEvent(orbitalName, event, payload, getTabClientId());
51277
- const effects = [];
51278
- const responseData = result.data || {};
51279
- const dataEntities = {};
51280
- for (const [entityName, records] of Object.entries(responseData)) {
51281
- dataEntities[entityName] = Array.isArray(records) ? records.length : 0;
51282
- }
51283
- const meta = {
51284
- success: !!result.success,
51285
- clientEffects: result.clientEffects?.length ?? 0,
51286
- dataEntities,
51287
- data: responseData,
51288
- emittedEvents: result.emittedEvents?.map((e) => e.event) ?? [],
51289
- error: result.error
51290
- };
51291
- if (result.success) {
51292
- const tagged = result.clientEffectsByTrait;
51293
- const tuples = tagged ? tagged.map((entry) => ({ effect: entry.effect, traitName: entry.traitName })) : (result.clientEffects ?? []).map((eff) => ({ effect: eff }));
51294
- for (const { effect, traitName } of tuples) {
51295
- const effectType = effect[0];
51296
- if (effectType === "render-ui") {
51297
- const slot = effect[1];
51298
- const pattern = effect[2];
51299
- effects.push({
51300
- type: "render-ui",
51301
- slot,
51302
- pattern: pattern !== null && typeof pattern === "object" ? pattern : void 0,
51303
- traitName
51304
- });
51305
- } else if (effectType === "navigate") {
51306
- const route = effect[1];
51307
- const rawParams = effect[2];
51308
- const rawOptions = effect[3];
51309
- const optionsObj = rawOptions !== null && rawOptions !== void 0 && typeof rawOptions === "object" && !Array.isArray(rawOptions) ? rawOptions : void 0;
51310
- const crumb = typeof optionsObj?.crumb === "string" ? optionsObj.crumb : void 0;
51311
- effects.push({
51312
- type: "navigate",
51313
- route,
51314
- params: rawParams !== null && rawParams !== void 0 && typeof rawParams === "object" && !Array.isArray(rawParams) ? rawParams : void 0,
51315
- crumb,
51316
- traitName
51317
- });
51318
- } else if (effectType === "navigate-back") {
51319
- effects.push({ type: "navigate-back", traitName });
51320
- } else if (effectType === "notify") {
51321
- const message = effect[1];
51322
- effects.push({ type: "notify", message: typeof message === "string" ? message : void 0, traitName });
51323
- }
51324
- }
51325
- if (result.emittedEvents) {
51326
- for (const emitted of result.emittedEvents) {
51327
- if (emitted.event === event) continue;
51328
- reEmitServerEvent(
51329
- eventBus,
51330
- { ...emitted, source: { ...emitted.source, dispatched: true } },
51331
- orbitalName
51332
- );
51623
+ if (tick !== void 0) {
51624
+ tickRelay.send(`${orbitalName}:${event}`, { orbitalName, event, payload, tick, sourceTrait });
51625
+ return { effects: [], meta: { ...emptyMeta, success: true } };
51626
+ }
51627
+ return commandPump.enqueue(async () => {
51628
+ if (disposedRef.current) return { effects: [], meta: emptyMeta };
51629
+ try {
51630
+ const result = await transport.sendEvent(orbitalName, event, payload, getTabClientId(), tick, sourceTrait);
51631
+ const effects = [];
51632
+ const responseData = result.data || {};
51633
+ const dataEntities = {};
51634
+ for (const [entityName, records] of Object.entries(responseData)) {
51635
+ dataEntities[entityName] = Array.isArray(records) ? records.length : 0;
51636
+ }
51637
+ const meta = {
51638
+ success: !!result.success,
51639
+ clientEffects: result.clientEffects?.length ?? 0,
51640
+ dataEntities,
51641
+ data: responseData,
51642
+ emittedEvents: result.emittedEvents?.map((e) => e.event) ?? [],
51643
+ error: result.error
51644
+ };
51645
+ if (result.success) {
51646
+ const tagged = result.clientEffectsByTrait;
51647
+ const tuples = tagged ? tagged.map((entry) => ({ effect: entry.effect, traitName: entry.traitName })) : (result.clientEffects ?? []).map((eff) => ({ effect: eff }));
51648
+ for (const { effect, traitName } of tuples) {
51649
+ const effectType = effect[0];
51650
+ if (effectType === "render-ui") {
51651
+ const slot = effect[1];
51652
+ const pattern = effect[2];
51653
+ effects.push({
51654
+ type: "render-ui",
51655
+ slot,
51656
+ pattern: pattern !== null && typeof pattern === "object" ? pattern : void 0,
51657
+ traitName
51658
+ });
51659
+ } else if (effectType === "navigate") {
51660
+ const route = effect[1];
51661
+ const rawParams = effect[2];
51662
+ const rawOptions = effect[3];
51663
+ const optionsObj = rawOptions !== null && rawOptions !== void 0 && typeof rawOptions === "object" && !Array.isArray(rawOptions) ? rawOptions : void 0;
51664
+ const crumb = typeof optionsObj?.crumb === "string" ? optionsObj.crumb : void 0;
51665
+ effects.push({
51666
+ type: "navigate",
51667
+ route,
51668
+ params: rawParams !== null && rawParams !== void 0 && typeof rawParams === "object" && !Array.isArray(rawParams) ? rawParams : void 0,
51669
+ crumb,
51670
+ traitName
51671
+ });
51672
+ } else if (effectType === "navigate-back") {
51673
+ effects.push({ type: "navigate-back", traitName });
51674
+ } else if (effectType === "notify") {
51675
+ const message = effect[1];
51676
+ effects.push({ type: "notify", message: typeof message === "string" ? message : void 0, traitName });
51677
+ }
51678
+ }
51679
+ if (result.emittedEvents) {
51680
+ for (const emitted of result.emittedEvents) {
51681
+ if (emitted.event === event) continue;
51682
+ reEmitServerEvent(
51683
+ eventBus,
51684
+ { ...emitted, source: { ...emitted.source, dispatched: true } },
51685
+ orbitalName
51686
+ );
51687
+ }
51333
51688
  }
51689
+ } else if (result.error) {
51690
+ xOrbitalLog.warn("response:fail", {
51691
+ orbital: orbitalName,
51692
+ event,
51693
+ error: result.error
51694
+ });
51334
51695
  }
51335
- } else if (result.error) {
51336
- xOrbitalLog.warn("response:fail", {
51337
- orbital: orbitalName,
51338
- event,
51339
- error: result.error
51340
- });
51341
- }
51342
- return { effects, meta };
51343
- } catch (err) {
51344
- const msg = err instanceof Error ? err.message : String(err);
51345
- if (err instanceof TypeError) {
51346
- xOrbitalLog.warn("response:network", {
51347
- orbital: orbitalName,
51348
- event,
51349
- error: msg,
51350
- reason: "peer endpoint unreachable (expected in standalone single-orbital mode)"
51351
- });
51352
- } else {
51353
- xOrbitalLog.error("response:network", {
51354
- orbital: orbitalName,
51355
- event,
51356
- error: msg
51357
- });
51696
+ return { effects, meta };
51697
+ } catch (err) {
51698
+ const msg = err instanceof Error ? err.message : String(err);
51699
+ if (err instanceof TypeError) {
51700
+ xOrbitalLog.warn("response:network", {
51701
+ orbital: orbitalName,
51702
+ event,
51703
+ error: msg,
51704
+ reason: "peer endpoint unreachable (expected in standalone single-orbital mode)"
51705
+ });
51706
+ } else {
51707
+ xOrbitalLog.error("response:network", {
51708
+ orbital: orbitalName,
51709
+ event,
51710
+ error: msg
51711
+ });
51712
+ }
51713
+ return { effects: [], meta: { ...emptyMeta, error: msg } };
51358
51714
  }
51359
- return { effects: [], meta: { ...emptyMeta, error: msg } };
51360
- }
51361
- }, [connected, transport, eventBus]);
51715
+ });
51716
+ }, [connected, transport, eventBus, tickRelay, commandPump]);
51362
51717
  useEffect(() => {
51363
51718
  if (!schema) return;
51364
51719
  let cancelled = false;