@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.
@@ -4664,11 +4664,54 @@ function isPlainObject(value) {
4664
4664
  if (typeof value === "function") return false;
4665
4665
  return true;
4666
4666
  }
4667
+ function isEvaluatorResolvedData(value) {
4668
+ return evaluatorResolvedData.has(value);
4669
+ }
4670
+ function brandResolved(value) {
4671
+ if (value !== null && typeof value === "object" && !React94__namespace.default.isValidElement(value) && !(value instanceof Date)) {
4672
+ resolvedMarkerFree.add(value);
4673
+ }
4674
+ }
4675
+ function subtreeHasMarker(value) {
4676
+ if (resolvedMarkerFree.has(value)) return false;
4677
+ const cached = markerPresenceCache.get(value);
4678
+ if (cached !== void 0) return cached;
4679
+ let found = false;
4680
+ const children = Array.isArray(value) ? value : Object.values(value);
4681
+ for (const child of children) {
4682
+ if (core.isRenderBindingMarker(child)) {
4683
+ found = true;
4684
+ break;
4685
+ }
4686
+ if (Array.isArray(child) || isPlainObject(child)) {
4687
+ if (subtreeHasMarker(child)) {
4688
+ found = true;
4689
+ break;
4690
+ }
4691
+ }
4692
+ }
4693
+ markerPresenceCache.set(value, found);
4694
+ return found;
4695
+ }
4667
4696
  function walkValue(value, scopeTrait, entity, config, state) {
4668
4697
  if (core.isRenderBindingMarker(value)) {
4669
- return { resolved: resolveMarkerExpression(value.expression, entity, config, state), changed: true };
4698
+ const cached = markerResolutionCache.get(value);
4699
+ if (cached !== void 0 && cached.entity === entity && cached.config === config && cached.state === state) {
4700
+ return { resolved: cached.resolved, changed: false };
4701
+ }
4702
+ const resolved = resolveMarkerExpression(value.expression, entity, config, state);
4703
+ markerResolutionCache.set(value, { entity, config, state, resolved });
4704
+ if (resolved !== null && typeof resolved === "object" && !React94__namespace.default.isValidElement(resolved) && !(resolved instanceof Date)) {
4705
+ resolvedMarkerFree.add(resolved);
4706
+ evaluatorResolvedData.add(resolved);
4707
+ }
4708
+ return { resolved, changed: true };
4670
4709
  }
4671
4710
  if (Array.isArray(value)) {
4711
+ if (!subtreeHasMarker(value)) {
4712
+ brandResolved(value);
4713
+ return { resolved: value, changed: false };
4714
+ }
4672
4715
  const out = [];
4673
4716
  let changed = false;
4674
4717
  for (const item of value) {
@@ -4683,9 +4726,14 @@ function walkValue(value, scopeTrait, entity, config, state) {
4683
4726
  out.push(resolved);
4684
4727
  if (itemChanged) changed = true;
4685
4728
  }
4729
+ brandResolved(out);
4686
4730
  return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
4687
4731
  }
4688
4732
  if (isPlainObject(value)) {
4733
+ if (!subtreeHasMarker(value)) {
4734
+ brandResolved(value);
4735
+ return { resolved: value, changed: false };
4736
+ }
4689
4737
  const sourceTrait = value._sourceTrait;
4690
4738
  if (typeof sourceTrait === "string" && sourceTrait !== scopeTrait) {
4691
4739
  return { resolved: value, changed: false };
@@ -4697,11 +4745,13 @@ function walkValue(value, scopeTrait, entity, config, state) {
4697
4745
  out[key] = resolved;
4698
4746
  if (itemChanged) changed = true;
4699
4747
  }
4748
+ brandResolved(out);
4700
4749
  return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
4701
4750
  }
4702
4751
  return { resolved: value, changed: false };
4703
4752
  }
4704
4753
  function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
4754
+ if (resolvedMarkerFree.has(props)) return props;
4705
4755
  const out = {};
4706
4756
  let changed = false;
4707
4757
  for (const [key, value] of Object.entries(props)) {
@@ -4709,11 +4759,17 @@ function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
4709
4759
  out[key] = resolved;
4710
4760
  if (propChanged) changed = true;
4711
4761
  }
4762
+ brandResolved(out);
4712
4763
  return changed ? out : props;
4713
4764
  }
4765
+ var markerPresenceCache, markerResolutionCache, resolvedMarkerFree, evaluatorResolvedData;
4714
4766
  var init_resolve_render_bindings = __esm({
4715
4767
  "lib/resolve-render-bindings.ts"() {
4716
4768
  "use client";
4769
+ markerPresenceCache = /* @__PURE__ */ new WeakMap();
4770
+ markerResolutionCache = /* @__PURE__ */ new WeakMap();
4771
+ resolvedMarkerFree = /* @__PURE__ */ new WeakSet();
4772
+ evaluatorResolvedData = /* @__PURE__ */ new WeakSet();
4717
4773
  }
4718
4774
  });
4719
4775
  function getCurrentIconFamily() {
@@ -15318,6 +15374,19 @@ var init_molecules = __esm({
15318
15374
  init_puzzleObject();
15319
15375
  }
15320
15376
  });
15377
+ var profilerOnRender;
15378
+ var init_perf = __esm({
15379
+ "lib/perf.ts"() {
15380
+ profilerOnRender = (id, phase, actualDuration, baseDuration, _startTime, commitTime) => {
15381
+ ui.pushPerfEntry({
15382
+ name: `profiler:${id}:${phase}`,
15383
+ durationMs: actualDuration,
15384
+ ts: commitTime,
15385
+ detail: { baseDuration }
15386
+ });
15387
+ };
15388
+ }
15389
+ });
15321
15390
  function themeBodyFont(el) {
15322
15391
  if (typeof getComputedStyle !== "function") return "system-ui, sans-serif";
15323
15392
  const v = getComputedStyle(el).getPropertyValue("--font-family-body").trim();
@@ -15666,6 +15735,7 @@ var init_LearningCanvas = __esm({
15666
15735
  "components/learning/atoms/LearningCanvas.tsx"() {
15667
15736
  "use client";
15668
15737
  init_cn();
15738
+ init_perf();
15669
15739
  init_useEventBus();
15670
15740
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
15671
15741
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
@@ -15709,6 +15779,7 @@ var init_LearningCanvas = __esm({
15709
15779
  return [...shapes, ...traceOut, ...readoutOut];
15710
15780
  }, [shapes, traces, readouts, width, height]);
15711
15781
  const draw = React94.useCallback(() => {
15782
+ const _perfT = ui.perfStart("learningcanvas:paint");
15712
15783
  const canvas = canvasRef.current;
15713
15784
  if (!canvas) return;
15714
15785
  const ctx = canvas.getContext("2d");
@@ -15730,6 +15801,7 @@ var init_LearningCanvas = __esm({
15730
15801
  for (const shape of derivedShapes) {
15731
15802
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
15732
15803
  }
15804
+ ui.perfEnd("learningcanvas:paint", _perfT);
15733
15805
  }, [width, height, backgroundColor, derivedShapes]);
15734
15806
  React94.useEffect(() => {
15735
15807
  draw();
@@ -29707,7 +29779,7 @@ function fileIcon(name) {
29707
29779
  return "file";
29708
29780
  }
29709
29781
  }
29710
- var TreeNodeItem, FileTree;
29782
+ var TreeNodeItem, FlatTreeNodeItem, FileTree;
29711
29783
  var init_FileTree = __esm({
29712
29784
  "components/core/molecules/FileTree.tsx"() {
29713
29785
  "use client";
@@ -29792,14 +29864,101 @@ var init_FileTree = __esm({
29792
29864
  )) })
29793
29865
  ] });
29794
29866
  };
29867
+ FlatTreeNodeItem = ({
29868
+ item,
29869
+ depth,
29870
+ indent,
29871
+ childrenByParent,
29872
+ onNodeSelect,
29873
+ defaultExpanded = false
29874
+ }) => {
29875
+ const [expanded, setExpanded] = React94.useState(defaultExpanded || depth < 1);
29876
+ const children = childrenByParent.get(item.id);
29877
+ const hasChildren = !!children && children.length > 0;
29878
+ const handleClick = React94.useCallback(() => {
29879
+ if (hasChildren) setExpanded((prev) => !prev);
29880
+ onNodeSelect?.(item.id);
29881
+ }, [hasChildren, item.id, onNodeSelect]);
29882
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
29883
+ /* @__PURE__ */ jsxRuntime.jsxs(
29884
+ Box,
29885
+ {
29886
+ className: "flex items-center gap-1.5 py-0.5 px-2 cursor-pointer rounded-sm transition-colors hover:bg-muted",
29887
+ style: { paddingLeft: depth * indent + 8 },
29888
+ onClick: handleClick,
29889
+ role: "treeitem",
29890
+ "aria-expanded": hasChildren ? expanded : void 0,
29891
+ children: [
29892
+ hasChildren ? /* @__PURE__ */ jsxRuntime.jsx(
29893
+ Icon,
29894
+ {
29895
+ name: expanded ? "chevron-down" : "chevron-right",
29896
+ size: "xs",
29897
+ className: "text-[var(--color-muted-foreground)] flex-shrink-0"
29898
+ }
29899
+ ) : /* @__PURE__ */ jsxRuntime.jsx(Box, { style: { width: 12, flexShrink: 0 } }),
29900
+ /* @__PURE__ */ jsxRuntime.jsx(
29901
+ Icon,
29902
+ {
29903
+ name: item.icon ?? (hasChildren ? expanded ? "folder-open" : "folder" : "file"),
29904
+ size: "xs",
29905
+ className: hasChildren ? "text-[var(--color-warning)]" : "text-[var(--color-muted-foreground)]"
29906
+ }
29907
+ ),
29908
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "truncate font-mono text-xs", children: item.label })
29909
+ ]
29910
+ }
29911
+ ),
29912
+ hasChildren && expanded && /* @__PURE__ */ jsxRuntime.jsx(Box, { role: "group", children: children.map((child) => /* @__PURE__ */ jsxRuntime.jsx(
29913
+ FlatTreeNodeItem,
29914
+ {
29915
+ item: child,
29916
+ depth: depth + 1,
29917
+ indent,
29918
+ childrenByParent,
29919
+ onNodeSelect
29920
+ },
29921
+ child.id
29922
+ )) })
29923
+ ] });
29924
+ };
29795
29925
  FileTree = ({
29796
29926
  tree,
29927
+ items,
29797
29928
  selectedPath,
29798
29929
  onFileSelect,
29930
+ onNodeSelect,
29799
29931
  className,
29800
29932
  indent = 16
29801
29933
  }) => {
29802
- if (tree.length === 0) return null;
29934
+ if (items) {
29935
+ if (items.length === 0) return null;
29936
+ const ids = new Set(items.map((node) => node.id));
29937
+ const childrenByParent = /* @__PURE__ */ new Map();
29938
+ const roots = [];
29939
+ for (const item of items) {
29940
+ if (item.parentId && ids.has(item.parentId)) {
29941
+ const siblings = childrenByParent.get(item.parentId);
29942
+ if (siblings) siblings.push(item);
29943
+ else childrenByParent.set(item.parentId, [item]);
29944
+ } else {
29945
+ roots.push(item);
29946
+ }
29947
+ }
29948
+ return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
29949
+ FlatTreeNodeItem,
29950
+ {
29951
+ item,
29952
+ depth: 0,
29953
+ indent,
29954
+ childrenByParent,
29955
+ onNodeSelect,
29956
+ defaultExpanded: true
29957
+ },
29958
+ item.id
29959
+ )) });
29960
+ }
29961
+ if (!tree || tree.length === 0) return null;
29803
29962
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: tree.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
29804
29963
  TreeNodeItem,
29805
29964
  {
@@ -31194,7 +31353,7 @@ var init_debug = __esm({
31194
31353
  logger.createLogger("almadar:ui:debug:game-state");
31195
31354
  }
31196
31355
  });
31197
- var isRelationsDebugEnabled, RelationSelect;
31356
+ var isRelationsDebugEnabled, MANY_CARDINALITIES, RelationSelect;
31198
31357
  var init_RelationSelect = __esm({
31199
31358
  "components/core/molecules/RelationSelect.tsx"() {
31200
31359
  "use client";
@@ -31208,6 +31367,11 @@ var init_RelationSelect = __esm({
31208
31367
  init_Typography();
31209
31368
  init_debug();
31210
31369
  isRelationsDebugEnabled = () => isDebugEnabled();
31370
+ MANY_CARDINALITIES = [
31371
+ "many",
31372
+ "one-to-many",
31373
+ "many-to-many"
31374
+ ];
31211
31375
  RelationSelect = ({
31212
31376
  value,
31213
31377
  onChange,
@@ -32832,6 +32996,7 @@ var init_MathCanvas = __esm({
32832
32996
  "components/learning/molecules/MathCanvas.tsx"() {
32833
32997
  "use client";
32834
32998
  init_useEventBus();
32999
+ init_perf();
32835
33000
  init_atoms();
32836
33001
  init_Stack();
32837
33002
  init_LearningCanvas();
@@ -32872,17 +33037,21 @@ var init_MathCanvas = __esm({
32872
33037
  error
32873
33038
  }) => {
32874
33039
  const eventBus = useEventBus();
33040
+ const keyMapKey = keyMap ? JSON.stringify(keyMap) : null;
33041
+ const keyUpMapKey = keyUpMap ? JSON.stringify(keyUpMap) : null;
33042
+ const stableKeyMap = React94.useMemo(() => keyMap, [keyMapKey]);
33043
+ const stableKeyUpMap = React94.useMemo(() => keyUpMap, [keyUpMapKey]);
32875
33044
  React94.useEffect(() => {
32876
- if (!keyMap && !keyUpMap) return;
33045
+ if (!stableKeyMap && !stableKeyUpMap) return;
32877
33046
  const onDown = (e) => {
32878
- const ev = keyMap?.[e.code];
33047
+ const ev = stableKeyMap?.[e.code];
32879
33048
  if (ev) {
32880
33049
  eventBus.emit(`UI:${ev}`, {});
32881
33050
  e.preventDefault();
32882
33051
  }
32883
33052
  };
32884
33053
  const onUp = (e) => {
32885
- const ev = keyUpMap?.[e.code];
33054
+ const ev = stableKeyUpMap?.[e.code];
32886
33055
  if (ev) eventBus.emit(`UI:${ev}`, {});
32887
33056
  };
32888
33057
  window.addEventListener("keydown", onDown);
@@ -32891,8 +33060,9 @@ var init_MathCanvas = __esm({
32891
33060
  window.removeEventListener("keydown", onDown);
32892
33061
  window.removeEventListener("keyup", onUp);
32893
33062
  };
32894
- }, [keyMap, keyUpMap, eventBus]);
33063
+ }, [stableKeyMap, stableKeyUpMap, eventBus]);
32895
33064
  const derivedShapes = React94.useMemo(() => {
33065
+ const _perfT = ui.perfStart("mathcanvas:derive");
32896
33066
  const out = [];
32897
33067
  const margin = 24;
32898
33068
  const plotW = width - margin * 2;
@@ -33136,6 +33306,7 @@ var init_MathCanvas = __esm({
33136
33306
  }
33137
33307
  }
33138
33308
  out.push(...shapes);
33309
+ ui.perfEnd("mathcanvas:derive", _perfT);
33139
33310
  return out;
33140
33311
  }, [
33141
33312
  width,
@@ -39489,7 +39660,7 @@ var init_RichBlockEditor = __esm({
39489
39660
  {
39490
39661
  variant: "bordered",
39491
39662
  padding: "none",
39492
- className: cn("flex flex-col", className),
39663
+ className: cn("flex flex-col text-card-foreground", className),
39493
39664
  children: [
39494
39665
  enableBlocks && showToolbar && !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
39495
39666
  Box,
@@ -44308,6 +44479,7 @@ var init_DetailPanel = __esm({
44308
44479
  "use client";
44309
44480
  init_atoms();
44310
44481
  init_Box();
44482
+ init_Input();
44311
44483
  init_Stack();
44312
44484
  init_SimpleGrid();
44313
44485
  init_Menu();
@@ -44323,6 +44495,7 @@ var init_DetailPanel = __esm({
44323
44495
  ReactMarkdown2 = React94.lazy(() => import('react-markdown'));
44324
44496
  DetailPanel = ({
44325
44497
  title: propTitle,
44498
+ onTitleCommit,
44326
44499
  subtitle,
44327
44500
  status,
44328
44501
  avatar,
@@ -44344,6 +44517,7 @@ var init_DetailPanel = __esm({
44344
44517
  }) => {
44345
44518
  const eventBus = useEventBus();
44346
44519
  const { t } = hooks.useTranslate();
44520
+ const [titleDraft, setTitleDraft] = React94__namespace.default.useState(null);
44347
44521
  const isFieldDefArray = (arr) => {
44348
44522
  if (!arr || arr.length === 0) return false;
44349
44523
  const first = arr[0];
@@ -44564,6 +44738,50 @@ var init_DetailPanel = __esm({
44564
44738
  }),
44565
44739
  status && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: status.variant ?? "default", children: status.label })
44566
44740
  ] });
44741
+ const commitTitle = () => {
44742
+ if (!onTitleCommit) return;
44743
+ const next = (titleDraft ?? "").trim();
44744
+ setTitleDraft(null);
44745
+ if (!next || next === title) return;
44746
+ onTitleCommit(next, normalizedData?.id !== void 0 ? String(normalizedData.id) : "");
44747
+ };
44748
+ const titleNode = onTitleCommit && titleDraft !== null ? /* @__PURE__ */ jsxRuntime.jsx(
44749
+ Input,
44750
+ {
44751
+ value: titleDraft,
44752
+ autoFocus: true,
44753
+ "aria-label": t("common.title"),
44754
+ className: "h-auto py-1 text-3xl font-bold tracking-tight",
44755
+ onChange: (e) => setTitleDraft(e.target.value),
44756
+ onBlur: commitTitle,
44757
+ onKeyDown: (e) => {
44758
+ if (e.key === "Enter") {
44759
+ e.preventDefault();
44760
+ commitTitle();
44761
+ } else if (e.key === "Escape") {
44762
+ e.preventDefault();
44763
+ setTitleDraft(null);
44764
+ }
44765
+ },
44766
+ "data-testid": "detail-title-input"
44767
+ }
44768
+ ) : onTitleCommit ? /* @__PURE__ */ jsxRuntime.jsx(
44769
+ Box,
44770
+ {
44771
+ role: "button",
44772
+ tabIndex: 0,
44773
+ className: "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
44774
+ onClick: () => setTitleDraft(title ?? ""),
44775
+ onKeyDown: (e) => {
44776
+ if (e.key === "Enter" || e.key === " ") {
44777
+ e.preventDefault();
44778
+ setTitleDraft(title ?? "");
44779
+ }
44780
+ },
44781
+ "data-testid": "detail-title-editable",
44782
+ children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" })
44783
+ }
44784
+ ) : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" });
44567
44785
  const content = /* @__PURE__ */ jsxRuntime.jsx(Card, { variant: "elevated", children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "md", className: "p-6", children: [
44568
44786
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { justify: "between", align: "start", gap: "md", children: [
44569
44787
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { align: "start", gap: "sm", className: "min-w-0", children: [
@@ -44583,7 +44801,7 @@ var init_DetailPanel = __esm({
44583
44801
  avatar,
44584
44802
  /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "xs", className: "min-w-0", children: [
44585
44803
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { align: "center", gap: "sm", wrap: true, children: [
44586
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" }),
44804
+ titleNode,
44587
44805
  statusBadges
44588
44806
  ] }),
44589
44807
  subtitle && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: subtitle })
@@ -44919,6 +45137,9 @@ function determineInputType(field) {
44919
45137
  if (field.type === "relation" || field.relation) {
44920
45138
  return "relation";
44921
45139
  }
45140
+ if (field.type === "array") {
45141
+ return "array";
45142
+ }
44922
45143
  if (field.type === "enum" || field.values || getEnumOptions(field).length > 0) {
44923
45144
  return "select";
44924
45145
  }
@@ -44994,6 +45215,7 @@ var init_Form = __esm({
44994
45215
  init_Typography();
44995
45216
  init_Icon();
44996
45217
  init_RelationSelect();
45218
+ init_TagInput();
44997
45219
  init_UploadDropZone();
44998
45220
  init_Alert();
44999
45221
  init_useEventBus();
@@ -45073,7 +45295,7 @@ var init_Form = __esm({
45073
45295
  values: "values" in f3 ? f3.values : void 0,
45074
45296
  min: f3.min,
45075
45297
  max: f3.max,
45076
- relation: "relation" in f3 ? { entity: f3.relation.entity } : void 0
45298
+ relation: "relation" in f3 ? { entity: f3.relation.entity, cardinality: f3.relation.cardinality } : void 0
45077
45299
  })
45078
45300
  );
45079
45301
  }, [entity, fields]);
@@ -45140,7 +45362,7 @@ var init_Form = __esm({
45140
45362
  });
45141
45363
  debug(
45142
45364
  "forms",
45143
- `Calculation triggered: ${calc.variableName} = ${value}`
45365
+ `Calculation triggered: ${calc.variableName} = ${String(value)}`
45144
45366
  );
45145
45367
  }
45146
45368
  });
@@ -45308,7 +45530,7 @@ var init_Form = __esm({
45308
45530
  values: "values" in entityField ? entityField.values : void 0,
45309
45531
  min: entityField.min,
45310
45532
  max: entityField.max,
45311
- relation: "relation" in entityField ? { entity: entityField.relation.entity } : void 0
45533
+ relation: "relation" in entityField ? { entity: entityField.relation.entity, cardinality: entityField.relation.cardinality } : void 0
45312
45534
  };
45313
45535
  }
45314
45536
  return { name: field, type: "string" };
@@ -45420,6 +45642,22 @@ var init_Form = __esm({
45420
45642
  case "relation": {
45421
45643
  const relationOptions = relationsData[fieldName] || [];
45422
45644
  const relationLoading = relationsLoading[fieldName] || false;
45645
+ if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
45646
+ const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
45647
+ return /* @__PURE__ */ jsxRuntime.jsx(
45648
+ Select,
45649
+ {
45650
+ ...commonProps,
45651
+ multiple: true,
45652
+ searchable: true,
45653
+ clearable: true,
45654
+ options: [...relationOptions],
45655
+ value: selectedValues,
45656
+ onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
45657
+ placeholder: field.placeholder || `Select ${label}...`
45658
+ }
45659
+ );
45660
+ }
45423
45661
  return /* @__PURE__ */ jsxRuntime.jsx(
45424
45662
  RelationSelect,
45425
45663
  {
@@ -45434,6 +45672,18 @@ var init_Form = __esm({
45434
45672
  }
45435
45673
  );
45436
45674
  }
45675
+ case "array": {
45676
+ const arrayValue = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : currentValue != null && currentValue !== "" ? [String(currentValue)] : [];
45677
+ return /* @__PURE__ */ jsxRuntime.jsx(
45678
+ TagInput,
45679
+ {
45680
+ placeholder: field.placeholder,
45681
+ disabled: isLoading,
45682
+ value: arrayValue,
45683
+ onChange: (next) => handleChange(fieldName, [...next])
45684
+ }
45685
+ );
45686
+ }
45437
45687
  case "number":
45438
45688
  return /* @__PURE__ */ jsxRuntime.jsx(
45439
45689
  Input,
@@ -51002,7 +51252,10 @@ function enrichFormFields(fields, entityDef) {
51002
51252
  enriched.values = entityField.enumValues;
51003
51253
  }
51004
51254
  if (entityField.relation) {
51005
- enriched.relation = entityField.relation.entity;
51255
+ enriched.relation = {
51256
+ entity: entityField.relation.entity,
51257
+ cardinality: entityField.relation.cardinality
51258
+ };
51006
51259
  }
51007
51260
  return enriched;
51008
51261
  }
@@ -51030,7 +51283,10 @@ function enrichFormFields(fields, entityDef) {
51030
51283
  }
51031
51284
  }
51032
51285
  if (!obj.relation && entityField.relation) {
51033
- enriched.relation = entityField.relation.entity;
51286
+ enriched.relation = {
51287
+ entity: entityField.relation.entity,
51288
+ cardinality: entityField.relation.cardinality
51289
+ };
51034
51290
  }
51035
51291
  return enriched;
51036
51292
  }
@@ -51045,7 +51301,12 @@ function enrichDetailFields(fields, entityDef) {
51045
51301
  const meta = { type: entityField.type };
51046
51302
  const values = entityField.values ?? entityField.enumValues;
51047
51303
  if (values && values.length > 0) meta.values = values;
51048
- if (entityField.relation) meta.relation = entityField.relation.entity;
51304
+ if (entityField.relation) {
51305
+ meta.relation = {
51306
+ entity: entityField.relation.entity,
51307
+ cardinality: entityField.relation.cardinality
51308
+ };
51309
+ }
51049
51310
  return meta;
51050
51311
  };
51051
51312
  return fields.map((field) => {
@@ -51599,6 +51860,33 @@ function isPlainConfigObject(value) {
51599
51860
  const proto = Object.getPrototypeOf(value);
51600
51861
  return proto === Object.prototype || proto === null;
51601
51862
  }
51863
+ function subtreeHasTraitRef(value) {
51864
+ if (isEvaluatorResolvedData(value)) return false;
51865
+ const cached = traitRefPresenceCache.get(value);
51866
+ if (cached !== void 0) return cached;
51867
+ let found = false;
51868
+ const children = Array.isArray(value) ? value : Object.values(value);
51869
+ for (const child of children) {
51870
+ if (typeof child === "string" && TRAIT_BINDING_RE.test(child)) {
51871
+ found = true;
51872
+ break;
51873
+ }
51874
+ if (core.isRenderBindingMarker(child)) continue;
51875
+ if (Array.isArray(child)) {
51876
+ if (subtreeHasTraitRef(child)) {
51877
+ found = true;
51878
+ break;
51879
+ }
51880
+ } else if (child !== null && typeof child === "object" && isPlainConfigObject(child)) {
51881
+ if (subtreeHasTraitRef(child)) {
51882
+ found = true;
51883
+ break;
51884
+ }
51885
+ }
51886
+ }
51887
+ traitRefPresenceCache.set(value, found);
51888
+ return found;
51889
+ }
51602
51890
  function substituteTraitRefsDeep(value, pathKey) {
51603
51891
  if (core.isRenderBindingMarker(value)) return value;
51604
51892
  if (typeof value === "string") {
@@ -51613,11 +51901,13 @@ function substituteTraitRefsDeep(value, pathKey) {
51613
51901
  return value;
51614
51902
  }
51615
51903
  if (Array.isArray(value)) {
51904
+ if (!subtreeHasTraitRef(value)) return value;
51616
51905
  return value.map(
51617
51906
  (item, i) => substituteTraitRefsDeep(item, `${pathKey}[${i}]`)
51618
51907
  );
51619
51908
  }
51620
51909
  if (typeof value === "object" && isPlainConfigObject(value)) {
51910
+ if (!subtreeHasTraitRef(value)) return value;
51621
51911
  const out = {};
51622
51912
  for (const [k, v] of Object.entries(value)) {
51623
51913
  out[k] = substituteTraitRefsDeep(v, `${pathKey}.${k}`);
@@ -51644,6 +51934,10 @@ function renderPatternProps(props, onDismiss, propsSchema) {
51644
51934
  };
51645
51935
  rendered[key] = /* @__PURE__ */ jsxRuntime.jsx(SlotContentRenderer, { content: childContent, onDismiss });
51646
51936
  } else if (Array.isArray(value)) {
51937
+ if (!isEvaluatorResolvedData(value) && value.some((el) => isPatternConfig(el))) ; else if (!subtreeHasTraitRef(value)) {
51938
+ rendered[key] = value;
51939
+ continue;
51940
+ }
51647
51941
  const isDataArray = propsSchema?.[key]?.items?.types?.includes("object") ?? false;
51648
51942
  rendered[key] = value.map((item, i) => {
51649
51943
  const el = item;
@@ -51906,7 +52200,7 @@ function UISlotRenderer({
51906
52200
  }
51907
52201
  return wrapped;
51908
52202
  }
51909
- var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext, SLOT_SKELETON_MAP, SELF_OVERLAY_PATTERNS, CONTENT_NODE_SLOTS, PATTERNS_WITH_CHILDREN;
52203
+ var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext, SLOT_SKELETON_MAP, SELF_OVERLAY_PATTERNS, CONTENT_NODE_SLOTS, PATTERNS_WITH_CHILDREN, traitRefPresenceCache;
51910
52204
  var init_UISlotRenderer = __esm({
51911
52205
  "components/core/organisms/UISlotRenderer.tsx"() {
51912
52206
  "use client";
@@ -51980,6 +52274,7 @@ var init_UISlotRenderer = __esm({
51980
52274
  "alert",
51981
52275
  "dialog"
51982
52276
  ]);
52277
+ traitRefPresenceCache = /* @__PURE__ */ new WeakMap();
51983
52278
  UISlotRenderer.displayName = "UISlotRenderer";
51984
52279
  }
51985
52280
  });
@@ -55654,7 +55949,22 @@ function createClientEffectHandlers(options) {
55654
55949
  };
55655
55950
  }
55656
55951
 
55952
+ // lib/event-queue-coalesce.ts
55953
+ function enqueueEvent(queue, entry) {
55954
+ if (entry.tick !== void 0) {
55955
+ const pending = queue.find(
55956
+ (e) => e.tick !== void 0 && e.eventKey === entry.eventKey && e.targetTrait === entry.targetTrait
55957
+ );
55958
+ if (pending !== void 0) {
55959
+ pending.payload = entry.payload;
55960
+ return;
55961
+ }
55962
+ }
55963
+ queue.push(entry);
55964
+ }
55965
+
55657
55966
  // hooks/useTraitStateMachine.ts
55967
+ init_perf();
55658
55968
  init_traitRegistry();
55659
55969
  init_verificationRegistry();
55660
55970
  var crossTraitLog = logger.createLogger("almadar:ui:cross-trait");
@@ -56228,12 +56538,13 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56228
56538
  entityId
56229
56539
  };
56230
56540
  const emittedDuringExec = [];
56541
+ const tickName = flushEvent.startsWith("tick:") ? flushEvent.slice(5) : void 0;
56231
56542
  const baseEmit = handlers.emit;
56232
56543
  const trackingHandlers = {
56233
56544
  ...handlers,
56234
56545
  emit: (event, eventPayload, source) => {
56235
56546
  emittedDuringExec.push(event);
56236
- baseEmit(event, eventPayload, source);
56547
+ baseEmit(event, eventPayload, tickName !== void 0 ? { ...source, tick: tickName } : source);
56237
56548
  }
56238
56549
  };
56239
56550
  if (traitName === "Hero") {
@@ -56340,6 +56651,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56340
56651
  }, [eventBus]);
56341
56652
  React94.useEffect(() => {
56342
56653
  const scheduler = runtime.createTickScheduler();
56654
+ const timedTick = (key, fn) => () => ui.perfTimeAsync(key, fn);
56343
56655
  const pureWriterTickKeys = /* @__PURE__ */ new Set();
56344
56656
  for (const group of sharedGroups.values()) {
56345
56657
  const ticksByInterval = /* @__PURE__ */ new Map();
@@ -56356,12 +56668,12 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56356
56668
  }
56357
56669
  for (const entries of ticksByInterval.values()) {
56358
56670
  const interval = entries[0].tick.interval;
56359
- const onDue = () => {
56671
+ const onDue = timedTick(`tick:shared:${group.storeKey}@${String(interval)}`, () => {
56360
56672
  const writers = entries.map(
56361
56673
  ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter)
56362
56674
  );
56363
56675
  runTickFrame(group.storeKey, writers, sharedEntityStore);
56364
- };
56676
+ });
56365
56677
  if (interval === "frame") {
56366
56678
  scheduler.add(0, onDue);
56367
56679
  } else if (typeof interval === "number") {
@@ -56379,21 +56691,23 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56379
56691
  if (sharedKey !== void 0 && pureWriterTickKeys.has(`${binding.trait.name}::${tick.name}`)) {
56380
56692
  continue;
56381
56693
  }
56694
+ const tickKey = `tick:${binding.trait.name}::${tick.name}`;
56382
56695
  if (tick.interval === "frame") {
56383
- scheduler.add(0, () => runTickEffects(tick, binding));
56696
+ scheduler.add(0, timedTick(tickKey, () => runTickEffects(tick, binding)));
56384
56697
  } else if (typeof tick.interval === "number") {
56385
- scheduler.add(tick.interval, () => runTickEffects(tick, binding));
56698
+ scheduler.add(tick.interval, timedTick(tickKey, () => runTickEffects(tick, binding)));
56386
56699
  } else if (runtime.isValidCronExpression(tick.interval)) {
56387
- scheduler.addCron(tick.interval, () => runTickEffects(tick, binding));
56700
+ scheduler.addCron(tick.interval, timedTick(tickKey, () => runTickEffects(tick, binding)));
56388
56701
  } else {
56389
- scheduler.add(runtime.parseDurationString(tick.interval), () => runTickEffects(tick, binding));
56702
+ scheduler.add(runtime.parseDurationString(tick.interval), timedTick(tickKey, () => runTickEffects(tick, binding)));
56390
56703
  }
56391
56704
  }
56392
56705
  }
56393
56706
  return () => scheduler.stopAll();
56394
56707
  }, [traitBindings, runTickEffects, sharedGroups, sharedEntityStore, emitFromSharedWriter]);
56395
- const processEventQueued = React94.useCallback(async (eventKey, payload, targetTrait) => {
56708
+ const processEventQueued = React94.useCallback(async (eventKey, payload, targetTrait, tick, sourceTrait) => {
56396
56709
  const normalizedEvent = normalizeEventKey(eventKey);
56710
+ const _perfT0 = ui.perfStart("processEvent:total");
56397
56711
  const bindings = traitBindingsRef.current;
56398
56712
  const currentManager = managerRef.current;
56399
56713
  crossTraitLog.debug("processEvent:enter", () => ({
@@ -56417,6 +56731,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56417
56731
  entityByTrait[name] = { ...sharedEntityStore.getSnapshot(sharedKey) };
56418
56732
  }
56419
56733
  }
56734
+ const _perfT1 = ui.perfStart("processEvent:guardMatch");
56420
56735
  const results = currentManager.sendEvent(
56421
56736
  normalizedEvent,
56422
56737
  payload,
@@ -56425,6 +56740,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56425
56740
  void 0,
56426
56741
  targetTrait
56427
56742
  );
56743
+ ui.perfEnd("processEvent:guardMatch", _perfT1);
56428
56744
  crossTraitLog.debug("processEvent:results", {
56429
56745
  event: normalizedEvent,
56430
56746
  executedCount: results.length,
@@ -56474,6 +56790,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56474
56790
  transition: `${result.previousState} -> ${result.newState}`,
56475
56791
  effects: JSON.stringify(result.effects)
56476
56792
  }));
56793
+ const _perfT2 = ui.perfStart("processEvent:executeAll");
56477
56794
  const emittedDuringExec = await executeTransitionEffects({
56478
56795
  binding,
56479
56796
  // upstream gap: /runtime TransitionResult.effects is unknown[] — they are SExpr at runtime (see Almadar_UI_Gaps.md)
@@ -56485,6 +56802,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56485
56802
  syncOnly: false,
56486
56803
  log: stateLog
56487
56804
  });
56805
+ ui.perfEnd("processEvent:executeAll", _perfT2);
56488
56806
  emittedByTrait.set(traitName, emittedDuringExec);
56489
56807
  for (const emittedKey of emittedDuringExec) {
56490
56808
  bridgeEchoPendingRef.current.set(
@@ -56581,23 +56899,31 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56581
56899
  if (orbital) dispatchedOrbitals.add(orbital);
56582
56900
  }
56583
56901
  const relayPayload = targetTrait !== void 0 ? { ...payload ?? {}, _targetTrait: targetTrait } : payload;
56584
- await onEventProcessed(normalizedEvent, relayPayload, dispatchedOrbitals);
56902
+ void onEventProcessed(normalizedEvent, relayPayload, dispatchedOrbitals, tick, sourceTrait);
56585
56903
  }
56904
+ ui.perfEnd("processEvent:total", _perfT0);
56905
+ ui.perfEnd(`event:${normalizedEvent}`, _perfT0);
56586
56906
  }, [entities, eventBus, sharedEntityStore]);
56587
56907
  const drainEventQueue = React94.useCallback(async () => {
56588
56908
  if (processingRef.current) return;
56589
56909
  processingRef.current = true;
56910
+ const _perfT = ui.perfStart("drain:pass");
56911
+ let _perfN = 0;
56590
56912
  try {
56591
56913
  while (eventQueueRef.current.length > 0) {
56592
56914
  const entry = eventQueueRef.current.shift();
56593
- await processEventQueued(entry.eventKey, entry.payload, entry.targetTrait);
56915
+ _perfN++;
56916
+ await processEventQueued(entry.eventKey, entry.payload, entry.targetTrait, entry.tick, entry.sourceTrait);
56594
56917
  }
56595
56918
  } finally {
56596
56919
  processingRef.current = false;
56920
+ ui.perfEnd("drain:pass", _perfT);
56921
+ ui.perfGauge("drain:passEntries", _perfN);
56597
56922
  }
56598
56923
  }, [processEventQueued]);
56599
- const enqueueAndDrain = React94.useCallback((eventKey, payload, targetTrait) => {
56600
- eventQueueRef.current.push({ eventKey, payload, targetTrait });
56924
+ const enqueueAndDrain = React94.useCallback((eventKey, payload, targetTrait, tick, sourceTrait) => {
56925
+ enqueueEvent(eventQueueRef.current, { eventKey, payload, targetTrait, tick, sourceTrait });
56926
+ ui.perfGauge("queue:depthAtEnqueue", eventQueueRef.current.length);
56601
56927
  void drainEventQueue();
56602
56928
  }, [drainEventQueue]);
56603
56929
  React94.useCallback((eventKey, payload) => {
@@ -56650,7 +56976,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56650
56976
  crossTraitLog.debug("self:fire-server-cascade", { traitName, busKey: selfBusKey, eventKey });
56651
56977
  }
56652
56978
  crossTraitLog.debug("self:fire", { traitName, busKey: selfBusKey, eventKey });
56653
- enqueueAndDrain(eventKey, event.payload, traitName);
56979
+ enqueueAndDrain(eventKey, event.payload, traitName, event.source?.tick, event.source?.trait);
56654
56980
  });
56655
56981
  unsubscribes.push(() => {
56656
56982
  crossTraitLog.debug("self:unsubscribe", { traitName, busKey: selfBusKey, eventKey });
@@ -56668,7 +56994,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56668
56994
  const bareKey = `UI:${eventKey}`;
56669
56995
  const unsub = eventBus.on(bareKey, (event) => {
56670
56996
  crossTraitLog.debug("bare-cascade:fire", { bareKey, eventKey });
56671
- enqueueAndDrain(eventKey, event.payload);
56997
+ enqueueAndDrain(eventKey, event.payload, void 0, event.source?.tick, event.source?.trait);
56672
56998
  });
56673
56999
  unsubscribes.push(() => {
56674
57000
  crossTraitLog.debug("bare-cascade:unsubscribe", { bareKey, eventKey });
@@ -56702,7 +57028,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56702
57028
  enqueueAndDrain(
56703
57029
  listen.triggers,
56704
57030
  core.applyListenPayloadMapping(listen.payloadMapping, event.payload, evaluator.evaluateListenPayloadExpr),
56705
- binding.trait.name
57031
+ binding.trait.name,
57032
+ event.source?.tick,
57033
+ event.source?.trait
56706
57034
  );
56707
57035
  });
56708
57036
  unsubscribes.push(() => {
@@ -56894,7 +57222,7 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
56894
57222
  [serverActiveTraits]
56895
57223
  );
56896
57224
  const uiSlots = context.useUISlots();
56897
- const onEventProcessed = React94.useCallback(async (event, payload, dispatchedOrbitals) => {
57225
+ const onEventProcessed = React94.useCallback((event, payload, dispatchedOrbitals, tick, sourceTrait) => {
56898
57226
  if (!bridge.connected || !orbitalNames?.length) return;
56899
57227
  const targets = dispatchedOrbitals && dispatchedOrbitals.size > 0 ? orbitalNames.filter((n) => dispatchedOrbitals.has(n)) : orbitalNames;
56900
57228
  xOrbitalLog.debug("TraitInitializer:fanout", () => ({
@@ -56904,9 +57232,14 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
56904
57232
  dispatchedOrbitalsSize: dispatchedOrbitals?.size ?? 0
56905
57233
  }));
56906
57234
  for (const name of targets) {
56907
- const { effects, meta } = await bridge.sendEvent(name, event, withActiveTraits(payload));
56908
- recordServerResponse(name, event, meta);
56909
- applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraitNames, onNavigateBack);
57235
+ if (tick !== void 0) {
57236
+ void bridge.sendEvent(name, event, withActiveTraits(payload), tick, sourceTrait);
57237
+ continue;
57238
+ }
57239
+ void bridge.sendEvent(name, event, withActiveTraits(payload)).then(({ effects, meta }) => {
57240
+ recordServerResponse(name, event, meta);
57241
+ applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraitNames, onNavigateBack);
57242
+ });
56910
57243
  }
56911
57244
  }, [bridge.connected, bridge.sendEvent, orbitalNames, uiSlots, onNavigate, onNavigateBack, embeddedTraits, activeTraitNames, withActiveTraits]);
56912
57245
  const opts = orbitalNames ? { onEventProcessed, navigate: onNavigate, navigateBack: onNavigateBack, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams } : { navigate: onNavigate, navigateBack: onNavigateBack, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams };
@@ -57224,14 +57557,15 @@ function OrbPreview({
57224
57557
  return pattern.replace(/:([A-Za-z0-9_]+)/g, (whole, key) => routeParams[key] ?? whole);
57225
57558
  }, [currentPagePath, routeParams]);
57226
57559
  const navStackRef = React94.useRef(null);
57227
- const handleNavigate = React94.useCallback((path) => {
57560
+ const handleNavigate = React94.useCallback((path, navState) => {
57228
57561
  const hit = providers.matchPathAmong(pages, path, (entry) => entry.page.path);
57229
57562
  const match = hit?.candidate;
57230
- const params = hit?.params ?? {};
57563
+ const params = { ...hit?.params ?? {}, ...navState ?? {} };
57231
57564
  navLog.debug("handleNavigate", () => ({
57232
57565
  path,
57233
57566
  matched: match?.page.name ?? null,
57234
57567
  params,
57568
+ navState: navState ? JSON.stringify(navState) : void 0,
57235
57569
  availablePaths: pages.map((p) => p.page.path)
57236
57570
  }));
57237
57571
  if (match?.page.name) {
@@ -57246,9 +57580,9 @@ function OrbPreview({
57246
57580
  }
57247
57581
  }, [pages]);
57248
57582
  const handleNavigateEffect = React94.useCallback(
57249
- (path, _params, crumb) => {
57583
+ (path, params, crumb) => {
57250
57584
  navStackRef.current?.beginNavigate(path, crumb);
57251
- handleNavigate(path);
57585
+ handleNavigate(path, params);
57252
57586
  },
57253
57587
  [handleNavigate]
57254
57588
  );
@@ -57379,11 +57713,13 @@ function BrowserPlayground({
57379
57713
  unregister: async () => {
57380
57714
  runtime.unregisterAll();
57381
57715
  },
57382
- sendEvent: async (orbitalName, event, payload) => {
57716
+ sendEvent: async (orbitalName, event, payload, _clientId, tick, sourceTrait) => {
57383
57717
  await registrationReady;
57384
57718
  return runtime.processOrbitalEvent(orbitalName, {
57385
57719
  event,
57386
- payload
57720
+ payload,
57721
+ tick,
57722
+ sourceTrait
57387
57723
  // @almadar/runtime OrbitalEventResponse.clientEffects uses a wider ClientEffectTuple than
57388
57724
  // ServerBridge's local definition — cast at this boundary (upstream fix queued).
57389
57725
  });
@@ -59563,14 +59899,7 @@ TraitCardNode.displayName = "TraitCardNode";
59563
59899
 
59564
59900
  // components/avl/organisms/FlowCanvas.tsx
59565
59901
  init_useEventBus();
59566
- var profilerOnRender = (id, phase, actualDuration, baseDuration, _startTime, commitTime) => {
59567
- ui.pushPerfEntry({
59568
- name: `profiler:${id}:${phase}`,
59569
- durationMs: actualDuration,
59570
- ts: commitTime,
59571
- detail: { baseDuration }
59572
- });
59573
- };
59902
+ init_perf();
59574
59903
  var flowCanvasLog = logger.createLogger("almadar:ui:flow-canvas");
59575
59904
  var NODE_TYPES = {
59576
59905
  preview: OrbPreviewNode,