@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.
@@ -360,11 +360,54 @@ function isPlainObject(value) {
360
360
  if (typeof value === "function") return false;
361
361
  return true;
362
362
  }
363
+ function isEvaluatorResolvedData(value) {
364
+ return evaluatorResolvedData.has(value);
365
+ }
366
+ function brandResolved(value) {
367
+ if (value !== null && typeof value === "object" && !React87__namespace.default.isValidElement(value) && !(value instanceof Date)) {
368
+ resolvedMarkerFree.add(value);
369
+ }
370
+ }
371
+ function subtreeHasMarker(value) {
372
+ if (resolvedMarkerFree.has(value)) return false;
373
+ const cached = markerPresenceCache.get(value);
374
+ if (cached !== void 0) return cached;
375
+ let found = false;
376
+ const children = Array.isArray(value) ? value : Object.values(value);
377
+ for (const child of children) {
378
+ if (core.isRenderBindingMarker(child)) {
379
+ found = true;
380
+ break;
381
+ }
382
+ if (Array.isArray(child) || isPlainObject(child)) {
383
+ if (subtreeHasMarker(child)) {
384
+ found = true;
385
+ break;
386
+ }
387
+ }
388
+ }
389
+ markerPresenceCache.set(value, found);
390
+ return found;
391
+ }
363
392
  function walkValue(value, scopeTrait, entity, config, state) {
364
393
  if (core.isRenderBindingMarker(value)) {
365
- return { resolved: resolveMarkerExpression(value.expression, entity, config, state), changed: true };
394
+ const cached = markerResolutionCache.get(value);
395
+ if (cached !== void 0 && cached.entity === entity && cached.config === config && cached.state === state) {
396
+ return { resolved: cached.resolved, changed: false };
397
+ }
398
+ const resolved = resolveMarkerExpression(value.expression, entity, config, state);
399
+ markerResolutionCache.set(value, { entity, config, state, resolved });
400
+ if (resolved !== null && typeof resolved === "object" && !React87__namespace.default.isValidElement(resolved) && !(resolved instanceof Date)) {
401
+ resolvedMarkerFree.add(resolved);
402
+ evaluatorResolvedData.add(resolved);
403
+ }
404
+ return { resolved, changed: true };
366
405
  }
367
406
  if (Array.isArray(value)) {
407
+ if (!subtreeHasMarker(value)) {
408
+ brandResolved(value);
409
+ return { resolved: value, changed: false };
410
+ }
368
411
  const out = [];
369
412
  let changed = false;
370
413
  for (const item of value) {
@@ -379,9 +422,14 @@ function walkValue(value, scopeTrait, entity, config, state) {
379
422
  out.push(resolved);
380
423
  if (itemChanged) changed = true;
381
424
  }
425
+ brandResolved(out);
382
426
  return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
383
427
  }
384
428
  if (isPlainObject(value)) {
429
+ if (!subtreeHasMarker(value)) {
430
+ brandResolved(value);
431
+ return { resolved: value, changed: false };
432
+ }
385
433
  const sourceTrait = value._sourceTrait;
386
434
  if (typeof sourceTrait === "string" && sourceTrait !== scopeTrait) {
387
435
  return { resolved: value, changed: false };
@@ -393,11 +441,13 @@ function walkValue(value, scopeTrait, entity, config, state) {
393
441
  out[key] = resolved;
394
442
  if (itemChanged) changed = true;
395
443
  }
444
+ brandResolved(out);
396
445
  return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
397
446
  }
398
447
  return { resolved: value, changed: false };
399
448
  }
400
449
  function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
450
+ if (resolvedMarkerFree.has(props)) return props;
401
451
  const out = {};
402
452
  let changed = false;
403
453
  for (const [key, value] of Object.entries(props)) {
@@ -405,11 +455,17 @@ function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
405
455
  out[key] = resolved;
406
456
  if (propChanged) changed = true;
407
457
  }
458
+ brandResolved(out);
408
459
  return changed ? out : props;
409
460
  }
461
+ var markerPresenceCache, markerResolutionCache, resolvedMarkerFree, evaluatorResolvedData;
410
462
  var init_resolve_render_bindings = __esm({
411
463
  "lib/resolve-render-bindings.ts"() {
412
464
  "use client";
465
+ markerPresenceCache = /* @__PURE__ */ new WeakMap();
466
+ markerResolutionCache = /* @__PURE__ */ new WeakMap();
467
+ resolvedMarkerFree = /* @__PURE__ */ new WeakSet();
468
+ evaluatorResolvedData = /* @__PURE__ */ new WeakSet();
413
469
  }
414
470
  });
415
471
  function cn(...inputs) {
@@ -11843,6 +11899,10 @@ var init_molecules = __esm({
11843
11899
  init_puzzleObject();
11844
11900
  }
11845
11901
  });
11902
+ var init_perf = __esm({
11903
+ "lib/perf.ts"() {
11904
+ }
11905
+ });
11846
11906
  function themeBodyFont(el) {
11847
11907
  if (typeof getComputedStyle !== "function") return "system-ui, sans-serif";
11848
11908
  const v = getComputedStyle(el).getPropertyValue("--font-family-body").trim();
@@ -12191,6 +12251,7 @@ var init_LearningCanvas = __esm({
12191
12251
  "components/learning/atoms/LearningCanvas.tsx"() {
12192
12252
  "use client";
12193
12253
  init_cn();
12254
+ init_perf();
12194
12255
  init_useEventBus();
12195
12256
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
12196
12257
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
@@ -12234,6 +12295,7 @@ var init_LearningCanvas = __esm({
12234
12295
  return [...shapes, ...traceOut, ...readoutOut];
12235
12296
  }, [shapes, traces, readouts, width, height]);
12236
12297
  const draw = React87.useCallback(() => {
12298
+ const _perfT = ui.perfStart("learningcanvas:paint");
12237
12299
  const canvas = canvasRef.current;
12238
12300
  if (!canvas) return;
12239
12301
  const ctx = canvas.getContext("2d");
@@ -12255,6 +12317,7 @@ var init_LearningCanvas = __esm({
12255
12317
  for (const shape of derivedShapes) {
12256
12318
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
12257
12319
  }
12320
+ ui.perfEnd("learningcanvas:paint", _perfT);
12258
12321
  }, [width, height, backgroundColor, derivedShapes]);
12259
12322
  React87.useEffect(() => {
12260
12323
  draw();
@@ -27442,7 +27505,7 @@ function fileIcon(name) {
27442
27505
  return "file";
27443
27506
  }
27444
27507
  }
27445
- var TreeNodeItem, FileTree;
27508
+ var TreeNodeItem, FlatTreeNodeItem, FileTree;
27446
27509
  var init_FileTree = __esm({
27447
27510
  "components/core/molecules/FileTree.tsx"() {
27448
27511
  "use client";
@@ -27527,14 +27590,101 @@ var init_FileTree = __esm({
27527
27590
  )) })
27528
27591
  ] });
27529
27592
  };
27593
+ FlatTreeNodeItem = ({
27594
+ item,
27595
+ depth,
27596
+ indent,
27597
+ childrenByParent,
27598
+ onNodeSelect,
27599
+ defaultExpanded = false
27600
+ }) => {
27601
+ const [expanded, setExpanded] = React87.useState(defaultExpanded || depth < 1);
27602
+ const children = childrenByParent.get(item.id);
27603
+ const hasChildren = !!children && children.length > 0;
27604
+ const handleClick = React87.useCallback(() => {
27605
+ if (hasChildren) setExpanded((prev) => !prev);
27606
+ onNodeSelect?.(item.id);
27607
+ }, [hasChildren, item.id, onNodeSelect]);
27608
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
27609
+ /* @__PURE__ */ jsxRuntime.jsxs(
27610
+ Box,
27611
+ {
27612
+ className: "flex items-center gap-1.5 py-0.5 px-2 cursor-pointer rounded-sm transition-colors hover:bg-muted",
27613
+ style: { paddingLeft: depth * indent + 8 },
27614
+ onClick: handleClick,
27615
+ role: "treeitem",
27616
+ "aria-expanded": hasChildren ? expanded : void 0,
27617
+ children: [
27618
+ hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(
27619
+ Icon,
27620
+ {
27621
+ name: expanded ? "chevron-down" : "chevron-right",
27622
+ size: "xs",
27623
+ className: "text-[var(--color-muted-foreground)] flex-shrink-0"
27624
+ }
27625
+ ) : /* @__PURE__ */ jsxRuntime.jsx(Box, { style: { width: 12, flexShrink: 0 } }),
27626
+ /* @__PURE__ */ jsxRuntime.jsx(
27627
+ Icon,
27628
+ {
27629
+ name: item.icon ?? (hasChildren ? expanded ? "folder-open" : "folder" : "file"),
27630
+ size: "xs",
27631
+ className: hasChildren ? "text-[var(--color-warning)]" : "text-[var(--color-muted-foreground)]"
27632
+ }
27633
+ ),
27634
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "truncate font-mono text-xs", children: item.label })
27635
+ ]
27636
+ }
27637
+ ),
27638
+ hasChildren && expanded && /* @__PURE__ */ jsxRuntime.jsx(Box, { role: "group", children: children.map((child) => /* @__PURE__ */ jsxRuntime.jsx(
27639
+ FlatTreeNodeItem,
27640
+ {
27641
+ item: child,
27642
+ depth: depth + 1,
27643
+ indent,
27644
+ childrenByParent,
27645
+ onNodeSelect
27646
+ },
27647
+ child.id
27648
+ )) })
27649
+ ] });
27650
+ };
27530
27651
  FileTree = ({
27531
27652
  tree,
27653
+ items,
27532
27654
  selectedPath,
27533
27655
  onFileSelect,
27656
+ onNodeSelect,
27534
27657
  className,
27535
27658
  indent = 16
27536
27659
  }) => {
27537
- if (tree.length === 0) return null;
27660
+ if (items) {
27661
+ if (items.length === 0) return null;
27662
+ const ids = new Set(items.map((node) => node.id));
27663
+ const childrenByParent = /* @__PURE__ */ new Map();
27664
+ const roots = [];
27665
+ for (const item of items) {
27666
+ if (item.parentId && ids.has(item.parentId)) {
27667
+ const siblings = childrenByParent.get(item.parentId);
27668
+ if (siblings) siblings.push(item);
27669
+ else childrenByParent.set(item.parentId, [item]);
27670
+ } else {
27671
+ roots.push(item);
27672
+ }
27673
+ }
27674
+ return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
27675
+ FlatTreeNodeItem,
27676
+ {
27677
+ item,
27678
+ depth: 0,
27679
+ indent,
27680
+ childrenByParent,
27681
+ onNodeSelect,
27682
+ defaultExpanded: true
27683
+ },
27684
+ item.id
27685
+ )) });
27686
+ }
27687
+ if (!tree || tree.length === 0) return null;
27538
27688
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: tree.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
27539
27689
  TreeNodeItem,
27540
27690
  {
@@ -28929,7 +29079,7 @@ var init_debug = __esm({
28929
29079
  logger.createLogger("almadar:ui:debug:game-state");
28930
29080
  }
28931
29081
  });
28932
- var isRelationsDebugEnabled, RelationSelect;
29082
+ var isRelationsDebugEnabled, MANY_CARDINALITIES, RelationSelect;
28933
29083
  var init_RelationSelect = __esm({
28934
29084
  "components/core/molecules/RelationSelect.tsx"() {
28935
29085
  "use client";
@@ -28943,6 +29093,11 @@ var init_RelationSelect = __esm({
28943
29093
  init_Typography();
28944
29094
  init_debug();
28945
29095
  isRelationsDebugEnabled = () => isDebugEnabled();
29096
+ MANY_CARDINALITIES = [
29097
+ "many",
29098
+ "one-to-many",
29099
+ "many-to-many"
29100
+ ];
28946
29101
  RelationSelect = ({
28947
29102
  value,
28948
29103
  onChange,
@@ -30567,6 +30722,7 @@ var init_MathCanvas = __esm({
30567
30722
  "components/learning/molecules/MathCanvas.tsx"() {
30568
30723
  "use client";
30569
30724
  init_useEventBus();
30725
+ init_perf();
30570
30726
  init_atoms();
30571
30727
  init_Stack();
30572
30728
  init_LearningCanvas();
@@ -30607,17 +30763,21 @@ var init_MathCanvas = __esm({
30607
30763
  error
30608
30764
  }) => {
30609
30765
  const eventBus = useEventBus();
30766
+ const keyMapKey = keyMap ? JSON.stringify(keyMap) : null;
30767
+ const keyUpMapKey = keyUpMap ? JSON.stringify(keyUpMap) : null;
30768
+ const stableKeyMap = React87.useMemo(() => keyMap, [keyMapKey]);
30769
+ const stableKeyUpMap = React87.useMemo(() => keyUpMap, [keyUpMapKey]);
30610
30770
  React87.useEffect(() => {
30611
- if (!keyMap && !keyUpMap) return;
30771
+ if (!stableKeyMap && !stableKeyUpMap) return;
30612
30772
  const onDown = (e) => {
30613
- const ev = keyMap?.[e.code];
30773
+ const ev = stableKeyMap?.[e.code];
30614
30774
  if (ev) {
30615
30775
  eventBus.emit(`UI:${ev}`, {});
30616
30776
  e.preventDefault();
30617
30777
  }
30618
30778
  };
30619
30779
  const onUp = (e) => {
30620
- const ev = keyUpMap?.[e.code];
30780
+ const ev = stableKeyUpMap?.[e.code];
30621
30781
  if (ev) eventBus.emit(`UI:${ev}`, {});
30622
30782
  };
30623
30783
  window.addEventListener("keydown", onDown);
@@ -30626,8 +30786,9 @@ var init_MathCanvas = __esm({
30626
30786
  window.removeEventListener("keydown", onDown);
30627
30787
  window.removeEventListener("keyup", onUp);
30628
30788
  };
30629
- }, [keyMap, keyUpMap, eventBus]);
30789
+ }, [stableKeyMap, stableKeyUpMap, eventBus]);
30630
30790
  const derivedShapes = React87.useMemo(() => {
30791
+ const _perfT = ui.perfStart("mathcanvas:derive");
30631
30792
  const out = [];
30632
30793
  const margin = 24;
30633
30794
  const plotW = width - margin * 2;
@@ -30871,6 +31032,7 @@ var init_MathCanvas = __esm({
30871
31032
  }
30872
31033
  }
30873
31034
  out.push(...shapes);
31035
+ ui.perfEnd("mathcanvas:derive", _perfT);
30874
31036
  return out;
30875
31037
  }, [
30876
31038
  width,
@@ -32134,12 +32296,12 @@ var init_MapView = __esm({
32134
32296
  shadowSize: [41, 41]
32135
32297
  });
32136
32298
  L.Marker.prototype.options.icon = defaultIcon;
32137
- const { useEffect: useEffect67, useRef: useRef64, useCallback: useCallback97, useState: useState95 } = React87__namespace.default;
32299
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback97, useState: useState95 } = React87__namespace.default;
32138
32300
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
32139
32301
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
32140
32302
  function MapUpdater({ centerLat, centerLng, zoom }) {
32141
32303
  const map = useMap();
32142
- const prevRef = useRef64({ centerLat, centerLng, zoom });
32304
+ const prevRef = useRef65({ centerLat, centerLng, zoom });
32143
32305
  useEffect67(() => {
32144
32306
  const prev = prevRef.current;
32145
32307
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -37224,7 +37386,7 @@ var init_RichBlockEditor = __esm({
37224
37386
  {
37225
37387
  variant: "bordered",
37226
37388
  padding: "none",
37227
- className: cn("flex flex-col", className),
37389
+ className: cn("flex flex-col text-card-foreground", className),
37228
37390
  children: [
37229
37391
  enableBlocks && showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
37230
37392
  Box,
@@ -42452,6 +42614,7 @@ var init_DetailPanel = __esm({
42452
42614
  "use client";
42453
42615
  init_atoms();
42454
42616
  init_Box();
42617
+ init_Input();
42455
42618
  init_Stack();
42456
42619
  init_SimpleGrid();
42457
42620
  init_Menu();
@@ -42467,6 +42630,7 @@ var init_DetailPanel = __esm({
42467
42630
  ReactMarkdown2 = React87.lazy(() => import('react-markdown'));
42468
42631
  DetailPanel = ({
42469
42632
  title: propTitle,
42633
+ onTitleCommit,
42470
42634
  subtitle,
42471
42635
  status,
42472
42636
  avatar,
@@ -42488,6 +42652,7 @@ var init_DetailPanel = __esm({
42488
42652
  }) => {
42489
42653
  const eventBus = useEventBus();
42490
42654
  const { t } = hooks.useTranslate();
42655
+ const [titleDraft, setTitleDraft] = React87__namespace.default.useState(null);
42491
42656
  const isFieldDefArray = (arr) => {
42492
42657
  if (!arr || arr.length === 0) return false;
42493
42658
  const first = arr[0];
@@ -42708,6 +42873,50 @@ var init_DetailPanel = __esm({
42708
42873
  }),
42709
42874
  status && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: status.variant ?? "default", children: status.label })
42710
42875
  ] });
42876
+ const commitTitle = () => {
42877
+ if (!onTitleCommit) return;
42878
+ const next = (titleDraft ?? "").trim();
42879
+ setTitleDraft(null);
42880
+ if (!next || next === title) return;
42881
+ onTitleCommit(next, normalizedData?.id !== void 0 ? String(normalizedData.id) : "");
42882
+ };
42883
+ const titleNode = onTitleCommit && titleDraft !== null ? /* @__PURE__ */ jsxRuntime.jsx(
42884
+ Input,
42885
+ {
42886
+ value: titleDraft,
42887
+ autoFocus: true,
42888
+ "aria-label": t("common.title"),
42889
+ className: "h-auto py-1 text-3xl font-bold tracking-tight",
42890
+ onChange: (e) => setTitleDraft(e.target.value),
42891
+ onBlur: commitTitle,
42892
+ onKeyDown: (e) => {
42893
+ if (e.key === "Enter") {
42894
+ e.preventDefault();
42895
+ commitTitle();
42896
+ } else if (e.key === "Escape") {
42897
+ e.preventDefault();
42898
+ setTitleDraft(null);
42899
+ }
42900
+ },
42901
+ "data-testid": "detail-title-input"
42902
+ }
42903
+ ) : onTitleCommit ? /* @__PURE__ */ jsxRuntime.jsx(
42904
+ Box,
42905
+ {
42906
+ role: "button",
42907
+ tabIndex: 0,
42908
+ className: "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
42909
+ onClick: () => setTitleDraft(title ?? ""),
42910
+ onKeyDown: (e) => {
42911
+ if (e.key === "Enter" || e.key === " ") {
42912
+ e.preventDefault();
42913
+ setTitleDraft(title ?? "");
42914
+ }
42915
+ },
42916
+ "data-testid": "detail-title-editable",
42917
+ children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" })
42918
+ }
42919
+ ) : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" });
42711
42920
  const content = /* @__PURE__ */ jsxRuntime.jsx(Card, { variant: "elevated", children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "md", className: "p-6", children: [
42712
42921
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { justify: "between", align: "start", gap: "md", children: [
42713
42922
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { align: "start", gap: "sm", className: "min-w-0", children: [
@@ -42727,7 +42936,7 @@ var init_DetailPanel = __esm({
42727
42936
  avatar,
42728
42937
  /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", className: "min-w-0", children: [
42729
42938
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { align: "center", gap: "sm", wrap: true, children: [
42730
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" }),
42939
+ titleNode,
42731
42940
  statusBadges
42732
42941
  ] }),
42733
42942
  subtitle && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: subtitle })
@@ -43063,6 +43272,9 @@ function determineInputType(field) {
43063
43272
  if (field.type === "relation" || field.relation) {
43064
43273
  return "relation";
43065
43274
  }
43275
+ if (field.type === "array") {
43276
+ return "array";
43277
+ }
43066
43278
  if (field.type === "enum" || field.values || getEnumOptions(field).length > 0) {
43067
43279
  return "select";
43068
43280
  }
@@ -43138,6 +43350,7 @@ var init_Form = __esm({
43138
43350
  init_Typography();
43139
43351
  init_Icon();
43140
43352
  init_RelationSelect();
43353
+ init_TagInput();
43141
43354
  init_UploadDropZone();
43142
43355
  init_Alert();
43143
43356
  init_useEventBus();
@@ -43217,7 +43430,7 @@ var init_Form = __esm({
43217
43430
  values: "values" in f3 ? f3.values : void 0,
43218
43431
  min: f3.min,
43219
43432
  max: f3.max,
43220
- relation: "relation" in f3 ? { entity: f3.relation.entity } : void 0
43433
+ relation: "relation" in f3 ? { entity: f3.relation.entity, cardinality: f3.relation.cardinality } : void 0
43221
43434
  })
43222
43435
  );
43223
43436
  }, [entity, fields]);
@@ -43284,7 +43497,7 @@ var init_Form = __esm({
43284
43497
  });
43285
43498
  debug(
43286
43499
  "forms",
43287
- `Calculation triggered: ${calc.variableName} = ${value}`
43500
+ `Calculation triggered: ${calc.variableName} = ${String(value)}`
43288
43501
  );
43289
43502
  }
43290
43503
  });
@@ -43452,7 +43665,7 @@ var init_Form = __esm({
43452
43665
  values: "values" in entityField ? entityField.values : void 0,
43453
43666
  min: entityField.min,
43454
43667
  max: entityField.max,
43455
- relation: "relation" in entityField ? { entity: entityField.relation.entity } : void 0
43668
+ relation: "relation" in entityField ? { entity: entityField.relation.entity, cardinality: entityField.relation.cardinality } : void 0
43456
43669
  };
43457
43670
  }
43458
43671
  return { name: field, type: "string" };
@@ -43564,6 +43777,22 @@ var init_Form = __esm({
43564
43777
  case "relation": {
43565
43778
  const relationOptions = relationsData[fieldName] || [];
43566
43779
  const relationLoading = relationsLoading[fieldName] || false;
43780
+ if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
43781
+ const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
43782
+ return /* @__PURE__ */ jsxRuntime.jsx(
43783
+ Select,
43784
+ {
43785
+ ...commonProps,
43786
+ multiple: true,
43787
+ searchable: true,
43788
+ clearable: true,
43789
+ options: [...relationOptions],
43790
+ value: selectedValues,
43791
+ onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
43792
+ placeholder: field.placeholder || `Select ${label}...`
43793
+ }
43794
+ );
43795
+ }
43567
43796
  return /* @__PURE__ */ jsxRuntime.jsx(
43568
43797
  RelationSelect,
43569
43798
  {
@@ -43578,6 +43807,18 @@ var init_Form = __esm({
43578
43807
  }
43579
43808
  );
43580
43809
  }
43810
+ case "array": {
43811
+ const arrayValue = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : currentValue != null && currentValue !== "" ? [String(currentValue)] : [];
43812
+ return /* @__PURE__ */ jsxRuntime.jsx(
43813
+ TagInput,
43814
+ {
43815
+ placeholder: field.placeholder,
43816
+ disabled: isLoading,
43817
+ value: arrayValue,
43818
+ onChange: (next) => handleChange(fieldName, [...next])
43819
+ }
43820
+ );
43821
+ }
43581
43822
  case "number":
43582
43823
  return /* @__PURE__ */ jsxRuntime.jsx(
43583
43824
  Input,
@@ -49127,7 +49368,10 @@ function enrichFormFields(fields, entityDef) {
49127
49368
  enriched.values = entityField.enumValues;
49128
49369
  }
49129
49370
  if (entityField.relation) {
49130
- enriched.relation = entityField.relation.entity;
49371
+ enriched.relation = {
49372
+ entity: entityField.relation.entity,
49373
+ cardinality: entityField.relation.cardinality
49374
+ };
49131
49375
  }
49132
49376
  return enriched;
49133
49377
  }
@@ -49155,7 +49399,10 @@ function enrichFormFields(fields, entityDef) {
49155
49399
  }
49156
49400
  }
49157
49401
  if (!obj.relation && entityField.relation) {
49158
- enriched.relation = entityField.relation.entity;
49402
+ enriched.relation = {
49403
+ entity: entityField.relation.entity,
49404
+ cardinality: entityField.relation.cardinality
49405
+ };
49159
49406
  }
49160
49407
  return enriched;
49161
49408
  }
@@ -49170,7 +49417,12 @@ function enrichDetailFields(fields, entityDef) {
49170
49417
  const meta = { type: entityField.type };
49171
49418
  const values = entityField.values ?? entityField.enumValues;
49172
49419
  if (values && values.length > 0) meta.values = values;
49173
- if (entityField.relation) meta.relation = entityField.relation.entity;
49420
+ if (entityField.relation) {
49421
+ meta.relation = {
49422
+ entity: entityField.relation.entity,
49423
+ cardinality: entityField.relation.cardinality
49424
+ };
49425
+ }
49174
49426
  return meta;
49175
49427
  };
49176
49428
  return fields.map((field) => {
@@ -49724,6 +49976,33 @@ function isPlainConfigObject(value) {
49724
49976
  const proto = Object.getPrototypeOf(value);
49725
49977
  return proto === Object.prototype || proto === null;
49726
49978
  }
49979
+ function subtreeHasTraitRef(value) {
49980
+ if (isEvaluatorResolvedData(value)) return false;
49981
+ const cached = traitRefPresenceCache.get(value);
49982
+ if (cached !== void 0) return cached;
49983
+ let found = false;
49984
+ const children = Array.isArray(value) ? value : Object.values(value);
49985
+ for (const child of children) {
49986
+ if (typeof child === "string" && TRAIT_BINDING_RE.test(child)) {
49987
+ found = true;
49988
+ break;
49989
+ }
49990
+ if (core.isRenderBindingMarker(child)) continue;
49991
+ if (Array.isArray(child)) {
49992
+ if (subtreeHasTraitRef(child)) {
49993
+ found = true;
49994
+ break;
49995
+ }
49996
+ } else if (child !== null && typeof child === "object" && isPlainConfigObject(child)) {
49997
+ if (subtreeHasTraitRef(child)) {
49998
+ found = true;
49999
+ break;
50000
+ }
50001
+ }
50002
+ }
50003
+ traitRefPresenceCache.set(value, found);
50004
+ return found;
50005
+ }
49727
50006
  function substituteTraitRefsDeep(value, pathKey) {
49728
50007
  if (core.isRenderBindingMarker(value)) return value;
49729
50008
  if (typeof value === "string") {
@@ -49738,11 +50017,13 @@ function substituteTraitRefsDeep(value, pathKey) {
49738
50017
  return value;
49739
50018
  }
49740
50019
  if (Array.isArray(value)) {
50020
+ if (!subtreeHasTraitRef(value)) return value;
49741
50021
  return value.map(
49742
50022
  (item, i) => substituteTraitRefsDeep(item, `${pathKey}[${i}]`)
49743
50023
  );
49744
50024
  }
49745
50025
  if (typeof value === "object" && isPlainConfigObject(value)) {
50026
+ if (!subtreeHasTraitRef(value)) return value;
49746
50027
  const out = {};
49747
50028
  for (const [k, v] of Object.entries(value)) {
49748
50029
  out[k] = substituteTraitRefsDeep(v, `${pathKey}.${k}`);
@@ -49769,6 +50050,10 @@ function renderPatternProps(props, onDismiss, propsSchema) {
49769
50050
  };
49770
50051
  rendered[key] = /* @__PURE__ */ jsxRuntime.jsx(SlotContentRenderer, { content: childContent, onDismiss });
49771
50052
  } else if (Array.isArray(value)) {
50053
+ if (!isEvaluatorResolvedData(value) && value.some((el) => isPatternConfig(el))) ; else if (!subtreeHasTraitRef(value)) {
50054
+ rendered[key] = value;
50055
+ continue;
50056
+ }
49772
50057
  const isDataArray = propsSchema?.[key]?.items?.types?.includes("object") ?? false;
49773
50058
  rendered[key] = value.map((item, i) => {
49774
50059
  const el = item;
@@ -50031,7 +50316,7 @@ function UISlotRenderer({
50031
50316
  }
50032
50317
  return wrapped;
50033
50318
  }
50034
- var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext, SLOT_SKELETON_MAP, SELF_OVERLAY_PATTERNS, CONTENT_NODE_SLOTS, PATTERNS_WITH_CHILDREN;
50319
+ var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext, SLOT_SKELETON_MAP, SELF_OVERLAY_PATTERNS, CONTENT_NODE_SLOTS, PATTERNS_WITH_CHILDREN, traitRefPresenceCache;
50035
50320
  var init_UISlotRenderer = __esm({
50036
50321
  "components/core/organisms/UISlotRenderer.tsx"() {
50037
50322
  "use client";
@@ -50105,6 +50390,7 @@ var init_UISlotRenderer = __esm({
50105
50390
  "alert",
50106
50391
  "dialog"
50107
50392
  ]);
50393
+ traitRefPresenceCache = /* @__PURE__ */ new WeakMap();
50108
50394
  UISlotRenderer.displayName = "UISlotRenderer";
50109
50395
  }
50110
50396
  });
@@ -51209,6 +51495,53 @@ function useNavigationId2() {
51209
51495
 
51210
51496
  // providers/ServerBridge.tsx
51211
51497
  init_useEventBus();
51498
+
51499
+ // lib/tick-send-relay.ts
51500
+ function createTickSendRelay(send) {
51501
+ const lanes = /* @__PURE__ */ new Map();
51502
+ const flush = (key, lane) => {
51503
+ const next = lane.pending;
51504
+ lane.pending = void 0;
51505
+ if (next === void 0) {
51506
+ lane.inFlight = false;
51507
+ return;
51508
+ }
51509
+ lane.inFlight = true;
51510
+ void send(key, next).catch(() => void 0).then(() => flush(key, lane));
51511
+ };
51512
+ return {
51513
+ send(key, value) {
51514
+ const lane = lanes.get(key) ?? { inFlight: false };
51515
+ lanes.set(key, lane);
51516
+ if (lane.inFlight) {
51517
+ lane.pending = value;
51518
+ return;
51519
+ }
51520
+ lane.pending = value;
51521
+ flush(key, lane);
51522
+ },
51523
+ clear() {
51524
+ for (const lane of lanes.values()) {
51525
+ lane.pending = void 0;
51526
+ }
51527
+ }
51528
+ };
51529
+ }
51530
+
51531
+ // lib/command-send-pump.ts
51532
+ function createCommandSendPump() {
51533
+ let tail = Promise.resolve();
51534
+ return {
51535
+ enqueue(job) {
51536
+ const result = tail.then(job);
51537
+ tail = result.then(
51538
+ () => void 0,
51539
+ () => void 0
51540
+ );
51541
+ return result;
51542
+ }
51543
+ };
51544
+ }
51212
51545
  var xOrbitalLog = logger.createLogger("almadar:runtime:cross-orbital");
51213
51546
  var serverBridgeLog = logger.createLogger("almadar:ui:server-bridge");
51214
51547
  function reEmitServerEvent(eventBus, emitted, origin) {
@@ -51297,8 +51630,8 @@ function createHttpTransport(serverUrl) {
51297
51630
  } catch {
51298
51631
  }
51299
51632
  },
51300
- sendEvent: async (orbitalName, event, payload, clientId) => {
51301
- const body = { event, payload, clientId };
51633
+ sendEvent: async (orbitalName, event, payload, clientId, tick, sourceTrait) => {
51634
+ const body = { event, payload, clientId, tick, sourceTrait };
51302
51635
  const res = await fetch(`${serverUrl}/${orbitalName}/events`, {
51303
51636
  method: "POST",
51304
51637
  headers: { "Content-Type": "application/json" },
@@ -51343,96 +51676,118 @@ function ServerBridgeProvider({
51343
51676
  async () => transport.unregister(),
51344
51677
  [transport]
51345
51678
  );
51346
- const sendEvent = React87.useCallback(async (orbitalName, event, payload) => {
51679
+ const tickRelay = React87.useMemo(
51680
+ () => createTickSendRelay(async (_key, snap) => {
51681
+ await transport.sendEvent(snap.orbitalName, snap.event, snap.payload, getTabClientId(), snap.tick, snap.sourceTrait);
51682
+ }),
51683
+ [transport]
51684
+ );
51685
+ React87.useEffect(() => () => tickRelay.clear(), [tickRelay]);
51686
+ const commandPump = React87.useMemo(createCommandSendPump, []);
51687
+ const disposedRef = React87.useRef(false);
51688
+ React87.useEffect(() => {
51689
+ disposedRef.current = false;
51690
+ return () => {
51691
+ disposedRef.current = true;
51692
+ };
51693
+ }, []);
51694
+ const sendEvent = React87.useCallback(async (orbitalName, event, payload, tick, sourceTrait) => {
51347
51695
  const emptyMeta = { success: false, clientEffects: 0, dataEntities: {}, emittedEvents: [] };
51348
51696
  if (!connected) return { effects: [], meta: emptyMeta };
51349
- try {
51350
- const result = await transport.sendEvent(orbitalName, event, payload, getTabClientId());
51351
- const effects = [];
51352
- const responseData = result.data || {};
51353
- const dataEntities = {};
51354
- for (const [entityName, records] of Object.entries(responseData)) {
51355
- dataEntities[entityName] = Array.isArray(records) ? records.length : 0;
51356
- }
51357
- const meta = {
51358
- success: !!result.success,
51359
- clientEffects: result.clientEffects?.length ?? 0,
51360
- dataEntities,
51361
- data: responseData,
51362
- emittedEvents: result.emittedEvents?.map((e) => e.event) ?? [],
51363
- error: result.error
51364
- };
51365
- if (result.success) {
51366
- const tagged = result.clientEffectsByTrait;
51367
- const tuples = tagged ? tagged.map((entry) => ({ effect: entry.effect, traitName: entry.traitName })) : (result.clientEffects ?? []).map((eff) => ({ effect: eff }));
51368
- for (const { effect, traitName } of tuples) {
51369
- const effectType = effect[0];
51370
- if (effectType === "render-ui") {
51371
- const slot = effect[1];
51372
- const pattern = effect[2];
51373
- effects.push({
51374
- type: "render-ui",
51375
- slot,
51376
- pattern: pattern !== null && typeof pattern === "object" ? pattern : void 0,
51377
- traitName
51378
- });
51379
- } else if (effectType === "navigate") {
51380
- const route = effect[1];
51381
- const rawParams = effect[2];
51382
- const rawOptions = effect[3];
51383
- const optionsObj = rawOptions !== null && rawOptions !== void 0 && typeof rawOptions === "object" && !Array.isArray(rawOptions) ? rawOptions : void 0;
51384
- const crumb = typeof optionsObj?.crumb === "string" ? optionsObj.crumb : void 0;
51385
- effects.push({
51386
- type: "navigate",
51387
- route,
51388
- params: rawParams !== null && rawParams !== void 0 && typeof rawParams === "object" && !Array.isArray(rawParams) ? rawParams : void 0,
51389
- crumb,
51390
- traitName
51391
- });
51392
- } else if (effectType === "navigate-back") {
51393
- effects.push({ type: "navigate-back", traitName });
51394
- } else if (effectType === "notify") {
51395
- const message = effect[1];
51396
- effects.push({ type: "notify", message: typeof message === "string" ? message : void 0, traitName });
51397
- }
51398
- }
51399
- if (result.emittedEvents) {
51400
- for (const emitted of result.emittedEvents) {
51401
- if (emitted.event === event) continue;
51402
- reEmitServerEvent(
51403
- eventBus,
51404
- { ...emitted, source: { ...emitted.source, dispatched: true } },
51405
- orbitalName
51406
- );
51697
+ if (tick !== void 0) {
51698
+ tickRelay.send(`${orbitalName}:${event}`, { orbitalName, event, payload, tick, sourceTrait });
51699
+ return { effects: [], meta: { ...emptyMeta, success: true } };
51700
+ }
51701
+ return commandPump.enqueue(async () => {
51702
+ if (disposedRef.current) return { effects: [], meta: emptyMeta };
51703
+ try {
51704
+ const result = await transport.sendEvent(orbitalName, event, payload, getTabClientId(), tick, sourceTrait);
51705
+ const effects = [];
51706
+ const responseData = result.data || {};
51707
+ const dataEntities = {};
51708
+ for (const [entityName, records] of Object.entries(responseData)) {
51709
+ dataEntities[entityName] = Array.isArray(records) ? records.length : 0;
51710
+ }
51711
+ const meta = {
51712
+ success: !!result.success,
51713
+ clientEffects: result.clientEffects?.length ?? 0,
51714
+ dataEntities,
51715
+ data: responseData,
51716
+ emittedEvents: result.emittedEvents?.map((e) => e.event) ?? [],
51717
+ error: result.error
51718
+ };
51719
+ if (result.success) {
51720
+ const tagged = result.clientEffectsByTrait;
51721
+ const tuples = tagged ? tagged.map((entry) => ({ effect: entry.effect, traitName: entry.traitName })) : (result.clientEffects ?? []).map((eff) => ({ effect: eff }));
51722
+ for (const { effect, traitName } of tuples) {
51723
+ const effectType = effect[0];
51724
+ if (effectType === "render-ui") {
51725
+ const slot = effect[1];
51726
+ const pattern = effect[2];
51727
+ effects.push({
51728
+ type: "render-ui",
51729
+ slot,
51730
+ pattern: pattern !== null && typeof pattern === "object" ? pattern : void 0,
51731
+ traitName
51732
+ });
51733
+ } else if (effectType === "navigate") {
51734
+ const route = effect[1];
51735
+ const rawParams = effect[2];
51736
+ const rawOptions = effect[3];
51737
+ const optionsObj = rawOptions !== null && rawOptions !== void 0 && typeof rawOptions === "object" && !Array.isArray(rawOptions) ? rawOptions : void 0;
51738
+ const crumb = typeof optionsObj?.crumb === "string" ? optionsObj.crumb : void 0;
51739
+ effects.push({
51740
+ type: "navigate",
51741
+ route,
51742
+ params: rawParams !== null && rawParams !== void 0 && typeof rawParams === "object" && !Array.isArray(rawParams) ? rawParams : void 0,
51743
+ crumb,
51744
+ traitName
51745
+ });
51746
+ } else if (effectType === "navigate-back") {
51747
+ effects.push({ type: "navigate-back", traitName });
51748
+ } else if (effectType === "notify") {
51749
+ const message = effect[1];
51750
+ effects.push({ type: "notify", message: typeof message === "string" ? message : void 0, traitName });
51751
+ }
51752
+ }
51753
+ if (result.emittedEvents) {
51754
+ for (const emitted of result.emittedEvents) {
51755
+ if (emitted.event === event) continue;
51756
+ reEmitServerEvent(
51757
+ eventBus,
51758
+ { ...emitted, source: { ...emitted.source, dispatched: true } },
51759
+ orbitalName
51760
+ );
51761
+ }
51407
51762
  }
51763
+ } else if (result.error) {
51764
+ xOrbitalLog.warn("response:fail", {
51765
+ orbital: orbitalName,
51766
+ event,
51767
+ error: result.error
51768
+ });
51408
51769
  }
51409
- } else if (result.error) {
51410
- xOrbitalLog.warn("response:fail", {
51411
- orbital: orbitalName,
51412
- event,
51413
- error: result.error
51414
- });
51415
- }
51416
- return { effects, meta };
51417
- } catch (err) {
51418
- const msg = err instanceof Error ? err.message : String(err);
51419
- if (err instanceof TypeError) {
51420
- xOrbitalLog.warn("response:network", {
51421
- orbital: orbitalName,
51422
- event,
51423
- error: msg,
51424
- reason: "peer endpoint unreachable (expected in standalone single-orbital mode)"
51425
- });
51426
- } else {
51427
- xOrbitalLog.error("response:network", {
51428
- orbital: orbitalName,
51429
- event,
51430
- error: msg
51431
- });
51770
+ return { effects, meta };
51771
+ } catch (err) {
51772
+ const msg = err instanceof Error ? err.message : String(err);
51773
+ if (err instanceof TypeError) {
51774
+ xOrbitalLog.warn("response:network", {
51775
+ orbital: orbitalName,
51776
+ event,
51777
+ error: msg,
51778
+ reason: "peer endpoint unreachable (expected in standalone single-orbital mode)"
51779
+ });
51780
+ } else {
51781
+ xOrbitalLog.error("response:network", {
51782
+ orbital: orbitalName,
51783
+ event,
51784
+ error: msg
51785
+ });
51786
+ }
51787
+ return { effects: [], meta: { ...emptyMeta, error: msg } };
51432
51788
  }
51433
- return { effects: [], meta: { ...emptyMeta, error: msg } };
51434
- }
51435
- }, [connected, transport, eventBus]);
51789
+ });
51790
+ }, [connected, transport, eventBus, tickRelay, commandPump]);
51436
51791
  React87.useEffect(() => {
51437
51792
  if (!schema) return;
51438
51793
  let cancelled = false;