@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.
package/dist/avl/index.js CHANGED
@@ -13,7 +13,7 @@ import { Loader2, X, Code, FileText, WrapText, Check, Copy, Lightbulb, CheckCirc
13
13
  import { createPortal } from 'react-dom';
14
14
  import { UISlotProvider, useUISlots, useTheme } from '@almadar/ui/context';
15
15
  import { evaluateGuard, evaluateListenPayloadExpr, evaluate, createMinimalContext, executeEffects } from '@almadar/evaluator';
16
- import { prepareSchemaForPreview, collectTraitRefsFromResolvedTrait, buildOrbitalsByTrait, collectEmbeddedTraits, wrapCallbackForEvent, pushPerfEntry, perfStart, perfEnd } from '@almadar/runtime/ui';
16
+ import { prepareSchemaForPreview, collectTraitRefsFromResolvedTrait, buildOrbitalsByTrait, collectEmbeddedTraits, perfStart, perfEnd, perfGauge, perfTimeAsync, wrapCallbackForEvent, pushPerfEntry } from '@almadar/runtime/ui';
17
17
  import { Link, Outlet, useLocation } from 'react-router-dom';
18
18
  import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light.js';
19
19
  import dark from 'react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus.js';
@@ -4588,11 +4588,54 @@ function isPlainObject(value) {
4588
4588
  if (typeof value === "function") return false;
4589
4589
  return true;
4590
4590
  }
4591
+ function isEvaluatorResolvedData(value) {
4592
+ return evaluatorResolvedData.has(value);
4593
+ }
4594
+ function brandResolved(value) {
4595
+ if (value !== null && typeof value === "object" && !React94__default.isValidElement(value) && !(value instanceof Date)) {
4596
+ resolvedMarkerFree.add(value);
4597
+ }
4598
+ }
4599
+ function subtreeHasMarker(value) {
4600
+ if (resolvedMarkerFree.has(value)) return false;
4601
+ const cached = markerPresenceCache.get(value);
4602
+ if (cached !== void 0) return cached;
4603
+ let found = false;
4604
+ const children = Array.isArray(value) ? value : Object.values(value);
4605
+ for (const child of children) {
4606
+ if (isRenderBindingMarker(child)) {
4607
+ found = true;
4608
+ break;
4609
+ }
4610
+ if (Array.isArray(child) || isPlainObject(child)) {
4611
+ if (subtreeHasMarker(child)) {
4612
+ found = true;
4613
+ break;
4614
+ }
4615
+ }
4616
+ }
4617
+ markerPresenceCache.set(value, found);
4618
+ return found;
4619
+ }
4591
4620
  function walkValue(value, scopeTrait, entity, config, state) {
4592
4621
  if (isRenderBindingMarker(value)) {
4593
- return { resolved: resolveMarkerExpression(value.expression, entity, config, state), changed: true };
4622
+ const cached = markerResolutionCache.get(value);
4623
+ if (cached !== void 0 && cached.entity === entity && cached.config === config && cached.state === state) {
4624
+ return { resolved: cached.resolved, changed: false };
4625
+ }
4626
+ const resolved = resolveMarkerExpression(value.expression, entity, config, state);
4627
+ markerResolutionCache.set(value, { entity, config, state, resolved });
4628
+ if (resolved !== null && typeof resolved === "object" && !React94__default.isValidElement(resolved) && !(resolved instanceof Date)) {
4629
+ resolvedMarkerFree.add(resolved);
4630
+ evaluatorResolvedData.add(resolved);
4631
+ }
4632
+ return { resolved, changed: true };
4594
4633
  }
4595
4634
  if (Array.isArray(value)) {
4635
+ if (!subtreeHasMarker(value)) {
4636
+ brandResolved(value);
4637
+ return { resolved: value, changed: false };
4638
+ }
4596
4639
  const out = [];
4597
4640
  let changed = false;
4598
4641
  for (const item of value) {
@@ -4607,9 +4650,14 @@ function walkValue(value, scopeTrait, entity, config, state) {
4607
4650
  out.push(resolved);
4608
4651
  if (itemChanged) changed = true;
4609
4652
  }
4653
+ brandResolved(out);
4610
4654
  return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
4611
4655
  }
4612
4656
  if (isPlainObject(value)) {
4657
+ if (!subtreeHasMarker(value)) {
4658
+ brandResolved(value);
4659
+ return { resolved: value, changed: false };
4660
+ }
4613
4661
  const sourceTrait = value._sourceTrait;
4614
4662
  if (typeof sourceTrait === "string" && sourceTrait !== scopeTrait) {
4615
4663
  return { resolved: value, changed: false };
@@ -4621,11 +4669,13 @@ function walkValue(value, scopeTrait, entity, config, state) {
4621
4669
  out[key] = resolved;
4622
4670
  if (itemChanged) changed = true;
4623
4671
  }
4672
+ brandResolved(out);
4624
4673
  return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
4625
4674
  }
4626
4675
  return { resolved: value, changed: false };
4627
4676
  }
4628
4677
  function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
4678
+ if (resolvedMarkerFree.has(props)) return props;
4629
4679
  const out = {};
4630
4680
  let changed = false;
4631
4681
  for (const [key, value] of Object.entries(props)) {
@@ -4633,11 +4683,17 @@ function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
4633
4683
  out[key] = resolved;
4634
4684
  if (propChanged) changed = true;
4635
4685
  }
4686
+ brandResolved(out);
4636
4687
  return changed ? out : props;
4637
4688
  }
4689
+ var markerPresenceCache, markerResolutionCache, resolvedMarkerFree, evaluatorResolvedData;
4638
4690
  var init_resolve_render_bindings = __esm({
4639
4691
  "lib/resolve-render-bindings.ts"() {
4640
4692
  "use client";
4693
+ markerPresenceCache = /* @__PURE__ */ new WeakMap();
4694
+ markerResolutionCache = /* @__PURE__ */ new WeakMap();
4695
+ resolvedMarkerFree = /* @__PURE__ */ new WeakSet();
4696
+ evaluatorResolvedData = /* @__PURE__ */ new WeakSet();
4641
4697
  }
4642
4698
  });
4643
4699
  function getCurrentIconFamily() {
@@ -15242,6 +15298,19 @@ var init_molecules = __esm({
15242
15298
  init_puzzleObject();
15243
15299
  }
15244
15300
  });
15301
+ var profilerOnRender;
15302
+ var init_perf = __esm({
15303
+ "lib/perf.ts"() {
15304
+ profilerOnRender = (id, phase, actualDuration, baseDuration, _startTime, commitTime) => {
15305
+ pushPerfEntry({
15306
+ name: `profiler:${id}:${phase}`,
15307
+ durationMs: actualDuration,
15308
+ ts: commitTime,
15309
+ detail: { baseDuration }
15310
+ });
15311
+ };
15312
+ }
15313
+ });
15245
15314
  function themeBodyFont(el) {
15246
15315
  if (typeof getComputedStyle !== "function") return "system-ui, sans-serif";
15247
15316
  const v = getComputedStyle(el).getPropertyValue("--font-family-body").trim();
@@ -15590,6 +15659,7 @@ var init_LearningCanvas = __esm({
15590
15659
  "components/learning/atoms/LearningCanvas.tsx"() {
15591
15660
  "use client";
15592
15661
  init_cn();
15662
+ init_perf();
15593
15663
  init_useEventBus();
15594
15664
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
15595
15665
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
@@ -15633,6 +15703,7 @@ var init_LearningCanvas = __esm({
15633
15703
  return [...shapes, ...traceOut, ...readoutOut];
15634
15704
  }, [shapes, traces, readouts, width, height]);
15635
15705
  const draw = useCallback(() => {
15706
+ const _perfT = perfStart("learningcanvas:paint");
15636
15707
  const canvas = canvasRef.current;
15637
15708
  if (!canvas) return;
15638
15709
  const ctx = canvas.getContext("2d");
@@ -15654,6 +15725,7 @@ var init_LearningCanvas = __esm({
15654
15725
  for (const shape of derivedShapes) {
15655
15726
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
15656
15727
  }
15728
+ perfEnd("learningcanvas:paint", _perfT);
15657
15729
  }, [width, height, backgroundColor, derivedShapes]);
15658
15730
  useEffect(() => {
15659
15731
  draw();
@@ -29631,7 +29703,7 @@ function fileIcon(name) {
29631
29703
  return "file";
29632
29704
  }
29633
29705
  }
29634
- var TreeNodeItem, FileTree;
29706
+ var TreeNodeItem, FlatTreeNodeItem, FileTree;
29635
29707
  var init_FileTree = __esm({
29636
29708
  "components/core/molecules/FileTree.tsx"() {
29637
29709
  "use client";
@@ -29716,14 +29788,101 @@ var init_FileTree = __esm({
29716
29788
  )) })
29717
29789
  ] });
29718
29790
  };
29791
+ FlatTreeNodeItem = ({
29792
+ item,
29793
+ depth,
29794
+ indent,
29795
+ childrenByParent,
29796
+ onNodeSelect,
29797
+ defaultExpanded = false
29798
+ }) => {
29799
+ const [expanded, setExpanded] = useState(defaultExpanded || depth < 1);
29800
+ const children = childrenByParent.get(item.id);
29801
+ const hasChildren = !!children && children.length > 0;
29802
+ const handleClick = useCallback(() => {
29803
+ if (hasChildren) setExpanded((prev) => !prev);
29804
+ onNodeSelect?.(item.id);
29805
+ }, [hasChildren, item.id, onNodeSelect]);
29806
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
29807
+ /* @__PURE__ */ jsxs(
29808
+ Box,
29809
+ {
29810
+ className: "flex items-center gap-1.5 py-0.5 px-2 cursor-pointer rounded-sm transition-colors hover:bg-muted",
29811
+ style: { paddingLeft: depth * indent + 8 },
29812
+ onClick: handleClick,
29813
+ role: "treeitem",
29814
+ "aria-expanded": hasChildren ? expanded : void 0,
29815
+ children: [
29816
+ hasChildren ? /* @__PURE__ */ jsx(
29817
+ Icon,
29818
+ {
29819
+ name: expanded ? "chevron-down" : "chevron-right",
29820
+ size: "xs",
29821
+ className: "text-[var(--color-muted-foreground)] flex-shrink-0"
29822
+ }
29823
+ ) : /* @__PURE__ */ jsx(Box, { style: { width: 12, flexShrink: 0 } }),
29824
+ /* @__PURE__ */ jsx(
29825
+ Icon,
29826
+ {
29827
+ name: item.icon ?? (hasChildren ? expanded ? "folder-open" : "folder" : "file"),
29828
+ size: "xs",
29829
+ className: hasChildren ? "text-[var(--color-warning)]" : "text-[var(--color-muted-foreground)]"
29830
+ }
29831
+ ),
29832
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "truncate font-mono text-xs", children: item.label })
29833
+ ]
29834
+ }
29835
+ ),
29836
+ hasChildren && expanded && /* @__PURE__ */ jsx(Box, { role: "group", children: children.map((child) => /* @__PURE__ */ jsx(
29837
+ FlatTreeNodeItem,
29838
+ {
29839
+ item: child,
29840
+ depth: depth + 1,
29841
+ indent,
29842
+ childrenByParent,
29843
+ onNodeSelect
29844
+ },
29845
+ child.id
29846
+ )) })
29847
+ ] });
29848
+ };
29719
29849
  FileTree = ({
29720
29850
  tree,
29851
+ items,
29721
29852
  selectedPath,
29722
29853
  onFileSelect,
29854
+ onNodeSelect,
29723
29855
  className,
29724
29856
  indent = 16
29725
29857
  }) => {
29726
- if (tree.length === 0) return null;
29858
+ if (items) {
29859
+ if (items.length === 0) return null;
29860
+ const ids = new Set(items.map((node) => node.id));
29861
+ const childrenByParent = /* @__PURE__ */ new Map();
29862
+ const roots = [];
29863
+ for (const item of items) {
29864
+ if (item.parentId && ids.has(item.parentId)) {
29865
+ const siblings = childrenByParent.get(item.parentId);
29866
+ if (siblings) siblings.push(item);
29867
+ else childrenByParent.set(item.parentId, [item]);
29868
+ } else {
29869
+ roots.push(item);
29870
+ }
29871
+ }
29872
+ return /* @__PURE__ */ jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: roots.map((item) => /* @__PURE__ */ jsx(
29873
+ FlatTreeNodeItem,
29874
+ {
29875
+ item,
29876
+ depth: 0,
29877
+ indent,
29878
+ childrenByParent,
29879
+ onNodeSelect,
29880
+ defaultExpanded: true
29881
+ },
29882
+ item.id
29883
+ )) });
29884
+ }
29885
+ if (!tree || tree.length === 0) return null;
29727
29886
  return /* @__PURE__ */ jsx(Box, { className: `py-1 overflow-y-auto ${className ?? ""}`, role: "tree", children: tree.map((node) => /* @__PURE__ */ jsx(
29728
29887
  TreeNodeItem,
29729
29888
  {
@@ -31118,7 +31277,7 @@ var init_debug = __esm({
31118
31277
  createLogger("almadar:ui:debug:game-state");
31119
31278
  }
31120
31279
  });
31121
- var isRelationsDebugEnabled, RelationSelect;
31280
+ var isRelationsDebugEnabled, MANY_CARDINALITIES, RelationSelect;
31122
31281
  var init_RelationSelect = __esm({
31123
31282
  "components/core/molecules/RelationSelect.tsx"() {
31124
31283
  "use client";
@@ -31132,6 +31291,11 @@ var init_RelationSelect = __esm({
31132
31291
  init_Typography();
31133
31292
  init_debug();
31134
31293
  isRelationsDebugEnabled = () => isDebugEnabled();
31294
+ MANY_CARDINALITIES = [
31295
+ "many",
31296
+ "one-to-many",
31297
+ "many-to-many"
31298
+ ];
31135
31299
  RelationSelect = ({
31136
31300
  value,
31137
31301
  onChange,
@@ -32756,6 +32920,7 @@ var init_MathCanvas = __esm({
32756
32920
  "components/learning/molecules/MathCanvas.tsx"() {
32757
32921
  "use client";
32758
32922
  init_useEventBus();
32923
+ init_perf();
32759
32924
  init_atoms();
32760
32925
  init_Stack();
32761
32926
  init_LearningCanvas();
@@ -32796,17 +32961,21 @@ var init_MathCanvas = __esm({
32796
32961
  error
32797
32962
  }) => {
32798
32963
  const eventBus = useEventBus();
32964
+ const keyMapKey = keyMap ? JSON.stringify(keyMap) : null;
32965
+ const keyUpMapKey = keyUpMap ? JSON.stringify(keyUpMap) : null;
32966
+ const stableKeyMap = useMemo(() => keyMap, [keyMapKey]);
32967
+ const stableKeyUpMap = useMemo(() => keyUpMap, [keyUpMapKey]);
32799
32968
  useEffect(() => {
32800
- if (!keyMap && !keyUpMap) return;
32969
+ if (!stableKeyMap && !stableKeyUpMap) return;
32801
32970
  const onDown = (e) => {
32802
- const ev = keyMap?.[e.code];
32971
+ const ev = stableKeyMap?.[e.code];
32803
32972
  if (ev) {
32804
32973
  eventBus.emit(`UI:${ev}`, {});
32805
32974
  e.preventDefault();
32806
32975
  }
32807
32976
  };
32808
32977
  const onUp = (e) => {
32809
- const ev = keyUpMap?.[e.code];
32978
+ const ev = stableKeyUpMap?.[e.code];
32810
32979
  if (ev) eventBus.emit(`UI:${ev}`, {});
32811
32980
  };
32812
32981
  window.addEventListener("keydown", onDown);
@@ -32815,8 +32984,9 @@ var init_MathCanvas = __esm({
32815
32984
  window.removeEventListener("keydown", onDown);
32816
32985
  window.removeEventListener("keyup", onUp);
32817
32986
  };
32818
- }, [keyMap, keyUpMap, eventBus]);
32987
+ }, [stableKeyMap, stableKeyUpMap, eventBus]);
32819
32988
  const derivedShapes = useMemo(() => {
32989
+ const _perfT = perfStart("mathcanvas:derive");
32820
32990
  const out = [];
32821
32991
  const margin = 24;
32822
32992
  const plotW = width - margin * 2;
@@ -33060,6 +33230,7 @@ var init_MathCanvas = __esm({
33060
33230
  }
33061
33231
  }
33062
33232
  out.push(...shapes);
33233
+ perfEnd("mathcanvas:derive", _perfT);
33063
33234
  return out;
33064
33235
  }, [
33065
33236
  width,
@@ -39413,7 +39584,7 @@ var init_RichBlockEditor = __esm({
39413
39584
  {
39414
39585
  variant: "bordered",
39415
39586
  padding: "none",
39416
- className: cn("flex flex-col", className),
39587
+ className: cn("flex flex-col text-card-foreground", className),
39417
39588
  children: [
39418
39589
  enableBlocks && showToolbar && !readOnly && /* @__PURE__ */ jsx(
39419
39590
  Box,
@@ -44232,6 +44403,7 @@ var init_DetailPanel = __esm({
44232
44403
  "use client";
44233
44404
  init_atoms();
44234
44405
  init_Box();
44406
+ init_Input();
44235
44407
  init_Stack();
44236
44408
  init_SimpleGrid();
44237
44409
  init_Menu();
@@ -44247,6 +44419,7 @@ var init_DetailPanel = __esm({
44247
44419
  ReactMarkdown2 = lazy(() => import('react-markdown'));
44248
44420
  DetailPanel = ({
44249
44421
  title: propTitle,
44422
+ onTitleCommit,
44250
44423
  subtitle,
44251
44424
  status,
44252
44425
  avatar,
@@ -44268,6 +44441,7 @@ var init_DetailPanel = __esm({
44268
44441
  }) => {
44269
44442
  const eventBus = useEventBus();
44270
44443
  const { t } = useTranslate();
44444
+ const [titleDraft, setTitleDraft] = React94__default.useState(null);
44271
44445
  const isFieldDefArray = (arr) => {
44272
44446
  if (!arr || arr.length === 0) return false;
44273
44447
  const first = arr[0];
@@ -44488,6 +44662,50 @@ var init_DetailPanel = __esm({
44488
44662
  }),
44489
44663
  status && /* @__PURE__ */ jsx(Badge, { variant: status.variant ?? "default", children: status.label })
44490
44664
  ] });
44665
+ const commitTitle = () => {
44666
+ if (!onTitleCommit) return;
44667
+ const next = (titleDraft ?? "").trim();
44668
+ setTitleDraft(null);
44669
+ if (!next || next === title) return;
44670
+ onTitleCommit(next, normalizedData?.id !== void 0 ? String(normalizedData.id) : "");
44671
+ };
44672
+ const titleNode = onTitleCommit && titleDraft !== null ? /* @__PURE__ */ jsx(
44673
+ Input,
44674
+ {
44675
+ value: titleDraft,
44676
+ autoFocus: true,
44677
+ "aria-label": t("common.title"),
44678
+ className: "h-auto py-1 text-3xl font-bold tracking-tight",
44679
+ onChange: (e) => setTitleDraft(e.target.value),
44680
+ onBlur: commitTitle,
44681
+ onKeyDown: (e) => {
44682
+ if (e.key === "Enter") {
44683
+ e.preventDefault();
44684
+ commitTitle();
44685
+ } else if (e.key === "Escape") {
44686
+ e.preventDefault();
44687
+ setTitleDraft(null);
44688
+ }
44689
+ },
44690
+ "data-testid": "detail-title-input"
44691
+ }
44692
+ ) : onTitleCommit ? /* @__PURE__ */ jsx(
44693
+ Box,
44694
+ {
44695
+ role: "button",
44696
+ tabIndex: 0,
44697
+ className: "cursor-text rounded px-1 -mx-1 transition-colors hover:bg-muted/40",
44698
+ onClick: () => setTitleDraft(title ?? ""),
44699
+ onKeyDown: (e) => {
44700
+ if (e.key === "Enter" || e.key === " ") {
44701
+ e.preventDefault();
44702
+ setTitleDraft(title ?? "");
44703
+ }
44704
+ },
44705
+ "data-testid": "detail-title-editable",
44706
+ children: /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" })
44707
+ }
44708
+ ) : /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" });
44491
44709
  const content = /* @__PURE__ */ jsx(Card, { variant: "elevated", children: /* @__PURE__ */ jsxs(VStack, { gap: "md", className: "p-6", children: [
44492
44710
  /* @__PURE__ */ jsxs(HStack, { justify: "between", align: "start", gap: "md", children: [
44493
44711
  /* @__PURE__ */ jsxs(HStack, { align: "start", gap: "sm", className: "min-w-0", children: [
@@ -44507,7 +44725,7 @@ var init_DetailPanel = __esm({
44507
44725
  avatar,
44508
44726
  /* @__PURE__ */ jsxs(VStack, { gap: "xs", className: "min-w-0", children: [
44509
44727
  /* @__PURE__ */ jsxs(HStack, { align: "center", gap: "sm", wrap: true, children: [
44510
- /* @__PURE__ */ jsx(Typography, { variant: "h2", weight: "bold", children: title || "Details" }),
44728
+ titleNode,
44511
44729
  statusBadges
44512
44730
  ] }),
44513
44731
  subtitle && /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: subtitle })
@@ -44843,6 +45061,9 @@ function determineInputType(field) {
44843
45061
  if (field.type === "relation" || field.relation) {
44844
45062
  return "relation";
44845
45063
  }
45064
+ if (field.type === "array") {
45065
+ return "array";
45066
+ }
44846
45067
  if (field.type === "enum" || field.values || getEnumOptions(field).length > 0) {
44847
45068
  return "select";
44848
45069
  }
@@ -44918,6 +45139,7 @@ var init_Form = __esm({
44918
45139
  init_Typography();
44919
45140
  init_Icon();
44920
45141
  init_RelationSelect();
45142
+ init_TagInput();
44921
45143
  init_UploadDropZone();
44922
45144
  init_Alert();
44923
45145
  init_useEventBus();
@@ -44997,7 +45219,7 @@ var init_Form = __esm({
44997
45219
  values: "values" in f3 ? f3.values : void 0,
44998
45220
  min: f3.min,
44999
45221
  max: f3.max,
45000
- relation: "relation" in f3 ? { entity: f3.relation.entity } : void 0
45222
+ relation: "relation" in f3 ? { entity: f3.relation.entity, cardinality: f3.relation.cardinality } : void 0
45001
45223
  })
45002
45224
  );
45003
45225
  }, [entity, fields]);
@@ -45064,7 +45286,7 @@ var init_Form = __esm({
45064
45286
  });
45065
45287
  debug(
45066
45288
  "forms",
45067
- `Calculation triggered: ${calc.variableName} = ${value}`
45289
+ `Calculation triggered: ${calc.variableName} = ${String(value)}`
45068
45290
  );
45069
45291
  }
45070
45292
  });
@@ -45232,7 +45454,7 @@ var init_Form = __esm({
45232
45454
  values: "values" in entityField ? entityField.values : void 0,
45233
45455
  min: entityField.min,
45234
45456
  max: entityField.max,
45235
- relation: "relation" in entityField ? { entity: entityField.relation.entity } : void 0
45457
+ relation: "relation" in entityField ? { entity: entityField.relation.entity, cardinality: entityField.relation.cardinality } : void 0
45236
45458
  };
45237
45459
  }
45238
45460
  return { name: field, type: "string" };
@@ -45344,6 +45566,22 @@ var init_Form = __esm({
45344
45566
  case "relation": {
45345
45567
  const relationOptions = relationsData[fieldName] || [];
45346
45568
  const relationLoading = relationsLoading[fieldName] || false;
45569
+ if (field.relation?.cardinality !== void 0 && MANY_CARDINALITIES.includes(field.relation.cardinality)) {
45570
+ const selectedValues = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : [];
45571
+ return /* @__PURE__ */ jsx(
45572
+ Select,
45573
+ {
45574
+ ...commonProps,
45575
+ multiple: true,
45576
+ searchable: true,
45577
+ clearable: true,
45578
+ options: [...relationOptions],
45579
+ value: selectedValues,
45580
+ onValueChange: (value) => handleChange(fieldName, Array.isArray(value) ? value : [value]),
45581
+ placeholder: field.placeholder || `Select ${label}...`
45582
+ }
45583
+ );
45584
+ }
45347
45585
  return /* @__PURE__ */ jsx(
45348
45586
  RelationSelect,
45349
45587
  {
@@ -45358,6 +45596,18 @@ var init_Form = __esm({
45358
45596
  }
45359
45597
  );
45360
45598
  }
45599
+ case "array": {
45600
+ const arrayValue = Array.isArray(currentValue) ? currentValue.map((v) => String(v)) : currentValue != null && currentValue !== "" ? [String(currentValue)] : [];
45601
+ return /* @__PURE__ */ jsx(
45602
+ TagInput,
45603
+ {
45604
+ placeholder: field.placeholder,
45605
+ disabled: isLoading,
45606
+ value: arrayValue,
45607
+ onChange: (next) => handleChange(fieldName, [...next])
45608
+ }
45609
+ );
45610
+ }
45361
45611
  case "number":
45362
45612
  return /* @__PURE__ */ jsx(
45363
45613
  Input,
@@ -50926,7 +51176,10 @@ function enrichFormFields(fields, entityDef) {
50926
51176
  enriched.values = entityField.enumValues;
50927
51177
  }
50928
51178
  if (entityField.relation) {
50929
- enriched.relation = entityField.relation.entity;
51179
+ enriched.relation = {
51180
+ entity: entityField.relation.entity,
51181
+ cardinality: entityField.relation.cardinality
51182
+ };
50930
51183
  }
50931
51184
  return enriched;
50932
51185
  }
@@ -50954,7 +51207,10 @@ function enrichFormFields(fields, entityDef) {
50954
51207
  }
50955
51208
  }
50956
51209
  if (!obj.relation && entityField.relation) {
50957
- enriched.relation = entityField.relation.entity;
51210
+ enriched.relation = {
51211
+ entity: entityField.relation.entity,
51212
+ cardinality: entityField.relation.cardinality
51213
+ };
50958
51214
  }
50959
51215
  return enriched;
50960
51216
  }
@@ -50969,7 +51225,12 @@ function enrichDetailFields(fields, entityDef) {
50969
51225
  const meta = { type: entityField.type };
50970
51226
  const values = entityField.values ?? entityField.enumValues;
50971
51227
  if (values && values.length > 0) meta.values = values;
50972
- if (entityField.relation) meta.relation = entityField.relation.entity;
51228
+ if (entityField.relation) {
51229
+ meta.relation = {
51230
+ entity: entityField.relation.entity,
51231
+ cardinality: entityField.relation.cardinality
51232
+ };
51233
+ }
50973
51234
  return meta;
50974
51235
  };
50975
51236
  return fields.map((field) => {
@@ -51523,6 +51784,33 @@ function isPlainConfigObject(value) {
51523
51784
  const proto = Object.getPrototypeOf(value);
51524
51785
  return proto === Object.prototype || proto === null;
51525
51786
  }
51787
+ function subtreeHasTraitRef(value) {
51788
+ if (isEvaluatorResolvedData(value)) return false;
51789
+ const cached = traitRefPresenceCache.get(value);
51790
+ if (cached !== void 0) return cached;
51791
+ let found = false;
51792
+ const children = Array.isArray(value) ? value : Object.values(value);
51793
+ for (const child of children) {
51794
+ if (typeof child === "string" && TRAIT_BINDING_RE.test(child)) {
51795
+ found = true;
51796
+ break;
51797
+ }
51798
+ if (isRenderBindingMarker(child)) continue;
51799
+ if (Array.isArray(child)) {
51800
+ if (subtreeHasTraitRef(child)) {
51801
+ found = true;
51802
+ break;
51803
+ }
51804
+ } else if (child !== null && typeof child === "object" && isPlainConfigObject(child)) {
51805
+ if (subtreeHasTraitRef(child)) {
51806
+ found = true;
51807
+ break;
51808
+ }
51809
+ }
51810
+ }
51811
+ traitRefPresenceCache.set(value, found);
51812
+ return found;
51813
+ }
51526
51814
  function substituteTraitRefsDeep(value, pathKey) {
51527
51815
  if (isRenderBindingMarker(value)) return value;
51528
51816
  if (typeof value === "string") {
@@ -51537,11 +51825,13 @@ function substituteTraitRefsDeep(value, pathKey) {
51537
51825
  return value;
51538
51826
  }
51539
51827
  if (Array.isArray(value)) {
51828
+ if (!subtreeHasTraitRef(value)) return value;
51540
51829
  return value.map(
51541
51830
  (item, i) => substituteTraitRefsDeep(item, `${pathKey}[${i}]`)
51542
51831
  );
51543
51832
  }
51544
51833
  if (typeof value === "object" && isPlainConfigObject(value)) {
51834
+ if (!subtreeHasTraitRef(value)) return value;
51545
51835
  const out = {};
51546
51836
  for (const [k, v] of Object.entries(value)) {
51547
51837
  out[k] = substituteTraitRefsDeep(v, `${pathKey}.${k}`);
@@ -51568,6 +51858,10 @@ function renderPatternProps(props, onDismiss, propsSchema) {
51568
51858
  };
51569
51859
  rendered[key] = /* @__PURE__ */ jsx(SlotContentRenderer, { content: childContent, onDismiss });
51570
51860
  } else if (Array.isArray(value)) {
51861
+ if (!isEvaluatorResolvedData(value) && value.some((el) => isPatternConfig(el))) ; else if (!subtreeHasTraitRef(value)) {
51862
+ rendered[key] = value;
51863
+ continue;
51864
+ }
51571
51865
  const isDataArray = propsSchema?.[key]?.items?.types?.includes("object") ?? false;
51572
51866
  rendered[key] = value.map((item, i) => {
51573
51867
  const el = item;
@@ -51830,7 +52124,7 @@ function UISlotRenderer({
51830
52124
  }
51831
52125
  return wrapped;
51832
52126
  }
51833
- var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext, SLOT_SKELETON_MAP, SELF_OVERLAY_PATTERNS, CONTENT_NODE_SLOTS, PATTERNS_WITH_CHILDREN;
52127
+ var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext, SLOT_SKELETON_MAP, SELF_OVERLAY_PATTERNS, CONTENT_NODE_SLOTS, PATTERNS_WITH_CHILDREN, traitRefPresenceCache;
51834
52128
  var init_UISlotRenderer = __esm({
51835
52129
  "components/core/organisms/UISlotRenderer.tsx"() {
51836
52130
  "use client";
@@ -51904,6 +52198,7 @@ var init_UISlotRenderer = __esm({
51904
52198
  "alert",
51905
52199
  "dialog"
51906
52200
  ]);
52201
+ traitRefPresenceCache = /* @__PURE__ */ new WeakMap();
51907
52202
  UISlotRenderer.displayName = "UISlotRenderer";
51908
52203
  }
51909
52204
  });
@@ -55578,7 +55873,22 @@ function createClientEffectHandlers(options) {
55578
55873
  };
55579
55874
  }
55580
55875
 
55876
+ // lib/event-queue-coalesce.ts
55877
+ function enqueueEvent(queue, entry) {
55878
+ if (entry.tick !== void 0) {
55879
+ const pending = queue.find(
55880
+ (e) => e.tick !== void 0 && e.eventKey === entry.eventKey && e.targetTrait === entry.targetTrait
55881
+ );
55882
+ if (pending !== void 0) {
55883
+ pending.payload = entry.payload;
55884
+ return;
55885
+ }
55886
+ }
55887
+ queue.push(entry);
55888
+ }
55889
+
55581
55890
  // hooks/useTraitStateMachine.ts
55891
+ init_perf();
55582
55892
  init_traitRegistry();
55583
55893
  init_verificationRegistry();
55584
55894
  var crossTraitLog = createLogger("almadar:ui:cross-trait");
@@ -56152,12 +56462,13 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56152
56462
  entityId
56153
56463
  };
56154
56464
  const emittedDuringExec = [];
56465
+ const tickName = flushEvent.startsWith("tick:") ? flushEvent.slice(5) : void 0;
56155
56466
  const baseEmit = handlers.emit;
56156
56467
  const trackingHandlers = {
56157
56468
  ...handlers,
56158
56469
  emit: (event, eventPayload, source) => {
56159
56470
  emittedDuringExec.push(event);
56160
- baseEmit(event, eventPayload, source);
56471
+ baseEmit(event, eventPayload, tickName !== void 0 ? { ...source, tick: tickName } : source);
56161
56472
  }
56162
56473
  };
56163
56474
  if (traitName === "Hero") {
@@ -56264,6 +56575,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56264
56575
  }, [eventBus]);
56265
56576
  useEffect(() => {
56266
56577
  const scheduler = createTickScheduler();
56578
+ const timedTick = (key, fn) => () => perfTimeAsync(key, fn);
56267
56579
  const pureWriterTickKeys = /* @__PURE__ */ new Set();
56268
56580
  for (const group of sharedGroups.values()) {
56269
56581
  const ticksByInterval = /* @__PURE__ */ new Map();
@@ -56280,12 +56592,12 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56280
56592
  }
56281
56593
  for (const entries of ticksByInterval.values()) {
56282
56594
  const interval = entries[0].tick.interval;
56283
- const onDue = () => {
56595
+ const onDue = timedTick(`tick:shared:${group.storeKey}@${String(interval)}`, () => {
56284
56596
  const writers = entries.map(
56285
56597
  ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter)
56286
56598
  );
56287
56599
  runTickFrame(group.storeKey, writers, sharedEntityStore);
56288
- };
56600
+ });
56289
56601
  if (interval === "frame") {
56290
56602
  scheduler.add(0, onDue);
56291
56603
  } else if (typeof interval === "number") {
@@ -56303,21 +56615,23 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56303
56615
  if (sharedKey !== void 0 && pureWriterTickKeys.has(`${binding.trait.name}::${tick.name}`)) {
56304
56616
  continue;
56305
56617
  }
56618
+ const tickKey = `tick:${binding.trait.name}::${tick.name}`;
56306
56619
  if (tick.interval === "frame") {
56307
- scheduler.add(0, () => runTickEffects(tick, binding));
56620
+ scheduler.add(0, timedTick(tickKey, () => runTickEffects(tick, binding)));
56308
56621
  } else if (typeof tick.interval === "number") {
56309
- scheduler.add(tick.interval, () => runTickEffects(tick, binding));
56622
+ scheduler.add(tick.interval, timedTick(tickKey, () => runTickEffects(tick, binding)));
56310
56623
  } else if (isValidCronExpression(tick.interval)) {
56311
- scheduler.addCron(tick.interval, () => runTickEffects(tick, binding));
56624
+ scheduler.addCron(tick.interval, timedTick(tickKey, () => runTickEffects(tick, binding)));
56312
56625
  } else {
56313
- scheduler.add(parseDurationString(tick.interval), () => runTickEffects(tick, binding));
56626
+ scheduler.add(parseDurationString(tick.interval), timedTick(tickKey, () => runTickEffects(tick, binding)));
56314
56627
  }
56315
56628
  }
56316
56629
  }
56317
56630
  return () => scheduler.stopAll();
56318
56631
  }, [traitBindings, runTickEffects, sharedGroups, sharedEntityStore, emitFromSharedWriter]);
56319
- const processEventQueued = useCallback(async (eventKey, payload, targetTrait) => {
56632
+ const processEventQueued = useCallback(async (eventKey, payload, targetTrait, tick, sourceTrait) => {
56320
56633
  const normalizedEvent = normalizeEventKey(eventKey);
56634
+ const _perfT0 = perfStart("processEvent:total");
56321
56635
  const bindings = traitBindingsRef.current;
56322
56636
  const currentManager = managerRef.current;
56323
56637
  crossTraitLog.debug("processEvent:enter", () => ({
@@ -56341,6 +56655,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56341
56655
  entityByTrait[name] = { ...sharedEntityStore.getSnapshot(sharedKey) };
56342
56656
  }
56343
56657
  }
56658
+ const _perfT1 = perfStart("processEvent:guardMatch");
56344
56659
  const results = currentManager.sendEvent(
56345
56660
  normalizedEvent,
56346
56661
  payload,
@@ -56349,6 +56664,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56349
56664
  void 0,
56350
56665
  targetTrait
56351
56666
  );
56667
+ perfEnd("processEvent:guardMatch", _perfT1);
56352
56668
  crossTraitLog.debug("processEvent:results", {
56353
56669
  event: normalizedEvent,
56354
56670
  executedCount: results.length,
@@ -56398,6 +56714,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56398
56714
  transition: `${result.previousState} -> ${result.newState}`,
56399
56715
  effects: JSON.stringify(result.effects)
56400
56716
  }));
56717
+ const _perfT2 = perfStart("processEvent:executeAll");
56401
56718
  const emittedDuringExec = await executeTransitionEffects({
56402
56719
  binding,
56403
56720
  // upstream gap: /runtime TransitionResult.effects is unknown[] — they are SExpr at runtime (see Almadar_UI_Gaps.md)
@@ -56409,6 +56726,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56409
56726
  syncOnly: false,
56410
56727
  log: stateLog
56411
56728
  });
56729
+ perfEnd("processEvent:executeAll", _perfT2);
56412
56730
  emittedByTrait.set(traitName, emittedDuringExec);
56413
56731
  for (const emittedKey of emittedDuringExec) {
56414
56732
  bridgeEchoPendingRef.current.set(
@@ -56505,23 +56823,31 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56505
56823
  if (orbital) dispatchedOrbitals.add(orbital);
56506
56824
  }
56507
56825
  const relayPayload = targetTrait !== void 0 ? { ...payload ?? {}, _targetTrait: targetTrait } : payload;
56508
- await onEventProcessed(normalizedEvent, relayPayload, dispatchedOrbitals);
56826
+ void onEventProcessed(normalizedEvent, relayPayload, dispatchedOrbitals, tick, sourceTrait);
56509
56827
  }
56828
+ perfEnd("processEvent:total", _perfT0);
56829
+ perfEnd(`event:${normalizedEvent}`, _perfT0);
56510
56830
  }, [entities, eventBus, sharedEntityStore]);
56511
56831
  const drainEventQueue = useCallback(async () => {
56512
56832
  if (processingRef.current) return;
56513
56833
  processingRef.current = true;
56834
+ const _perfT = perfStart("drain:pass");
56835
+ let _perfN = 0;
56514
56836
  try {
56515
56837
  while (eventQueueRef.current.length > 0) {
56516
56838
  const entry = eventQueueRef.current.shift();
56517
- await processEventQueued(entry.eventKey, entry.payload, entry.targetTrait);
56839
+ _perfN++;
56840
+ await processEventQueued(entry.eventKey, entry.payload, entry.targetTrait, entry.tick, entry.sourceTrait);
56518
56841
  }
56519
56842
  } finally {
56520
56843
  processingRef.current = false;
56844
+ perfEnd("drain:pass", _perfT);
56845
+ perfGauge("drain:passEntries", _perfN);
56521
56846
  }
56522
56847
  }, [processEventQueued]);
56523
- const enqueueAndDrain = useCallback((eventKey, payload, targetTrait) => {
56524
- eventQueueRef.current.push({ eventKey, payload, targetTrait });
56848
+ const enqueueAndDrain = useCallback((eventKey, payload, targetTrait, tick, sourceTrait) => {
56849
+ enqueueEvent(eventQueueRef.current, { eventKey, payload, targetTrait, tick, sourceTrait });
56850
+ perfGauge("queue:depthAtEnqueue", eventQueueRef.current.length);
56525
56851
  void drainEventQueue();
56526
56852
  }, [drainEventQueue]);
56527
56853
  useCallback((eventKey, payload) => {
@@ -56574,7 +56900,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56574
56900
  crossTraitLog.debug("self:fire-server-cascade", { traitName, busKey: selfBusKey, eventKey });
56575
56901
  }
56576
56902
  crossTraitLog.debug("self:fire", { traitName, busKey: selfBusKey, eventKey });
56577
- enqueueAndDrain(eventKey, event.payload, traitName);
56903
+ enqueueAndDrain(eventKey, event.payload, traitName, event.source?.tick, event.source?.trait);
56578
56904
  });
56579
56905
  unsubscribes.push(() => {
56580
56906
  crossTraitLog.debug("self:unsubscribe", { traitName, busKey: selfBusKey, eventKey });
@@ -56592,7 +56918,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56592
56918
  const bareKey = `UI:${eventKey}`;
56593
56919
  const unsub = eventBus.on(bareKey, (event) => {
56594
56920
  crossTraitLog.debug("bare-cascade:fire", { bareKey, eventKey });
56595
- enqueueAndDrain(eventKey, event.payload);
56921
+ enqueueAndDrain(eventKey, event.payload, void 0, event.source?.tick, event.source?.trait);
56596
56922
  });
56597
56923
  unsubscribes.push(() => {
56598
56924
  crossTraitLog.debug("bare-cascade:unsubscribe", { bareKey, eventKey });
@@ -56626,7 +56952,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
56626
56952
  enqueueAndDrain(
56627
56953
  listen.triggers,
56628
56954
  applyListenPayloadMapping(listen.payloadMapping, event.payload, evaluateListenPayloadExpr),
56629
- binding.trait.name
56955
+ binding.trait.name,
56956
+ event.source?.tick,
56957
+ event.source?.trait
56630
56958
  );
56631
56959
  });
56632
56960
  unsubscribes.push(() => {
@@ -56818,7 +57146,7 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
56818
57146
  [serverActiveTraits]
56819
57147
  );
56820
57148
  const uiSlots = useUISlots();
56821
- const onEventProcessed = useCallback(async (event, payload, dispatchedOrbitals) => {
57149
+ const onEventProcessed = useCallback((event, payload, dispatchedOrbitals, tick, sourceTrait) => {
56822
57150
  if (!bridge.connected || !orbitalNames?.length) return;
56823
57151
  const targets = dispatchedOrbitals && dispatchedOrbitals.size > 0 ? orbitalNames.filter((n) => dispatchedOrbitals.has(n)) : orbitalNames;
56824
57152
  xOrbitalLog.debug("TraitInitializer:fanout", () => ({
@@ -56828,9 +57156,14 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
56828
57156
  dispatchedOrbitalsSize: dispatchedOrbitals?.size ?? 0
56829
57157
  }));
56830
57158
  for (const name of targets) {
56831
- const { effects, meta } = await bridge.sendEvent(name, event, withActiveTraits(payload));
56832
- recordServerResponse(name, event, meta);
56833
- applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraitNames, onNavigateBack);
57159
+ if (tick !== void 0) {
57160
+ void bridge.sendEvent(name, event, withActiveTraits(payload), tick, sourceTrait);
57161
+ continue;
57162
+ }
57163
+ void bridge.sendEvent(name, event, withActiveTraits(payload)).then(({ effects, meta }) => {
57164
+ recordServerResponse(name, event, meta);
57165
+ applyServerEffects(effects, uiSlots, onNavigate, embeddedTraits, activeTraitNames, onNavigateBack);
57166
+ });
56834
57167
  }
56835
57168
  }, [bridge.connected, bridge.sendEvent, orbitalNames, uiSlots, onNavigate, onNavigateBack, embeddedTraits, activeTraitNames, withActiveTraits]);
56836
57169
  const opts = orbitalNames ? { onEventProcessed, navigate: onNavigate, navigateBack: onNavigateBack, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams } : { navigate: onNavigate, navigateBack: onNavigateBack, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams };
@@ -57148,14 +57481,15 @@ function OrbPreview({
57148
57481
  return pattern.replace(/:([A-Za-z0-9_]+)/g, (whole, key) => routeParams[key] ?? whole);
57149
57482
  }, [currentPagePath, routeParams]);
57150
57483
  const navStackRef = useRef(null);
57151
- const handleNavigate = useCallback((path) => {
57484
+ const handleNavigate = useCallback((path, navState) => {
57152
57485
  const hit = matchPathAmong(pages, path, (entry) => entry.page.path);
57153
57486
  const match = hit?.candidate;
57154
- const params = hit?.params ?? {};
57487
+ const params = { ...hit?.params ?? {}, ...navState ?? {} };
57155
57488
  navLog.debug("handleNavigate", () => ({
57156
57489
  path,
57157
57490
  matched: match?.page.name ?? null,
57158
57491
  params,
57492
+ navState: navState ? JSON.stringify(navState) : void 0,
57159
57493
  availablePaths: pages.map((p) => p.page.path)
57160
57494
  }));
57161
57495
  if (match?.page.name) {
@@ -57170,9 +57504,9 @@ function OrbPreview({
57170
57504
  }
57171
57505
  }, [pages]);
57172
57506
  const handleNavigateEffect = useCallback(
57173
- (path, _params, crumb) => {
57507
+ (path, params, crumb) => {
57174
57508
  navStackRef.current?.beginNavigate(path, crumb);
57175
- handleNavigate(path);
57509
+ handleNavigate(path, params);
57176
57510
  },
57177
57511
  [handleNavigate]
57178
57512
  );
@@ -57303,11 +57637,13 @@ function BrowserPlayground({
57303
57637
  unregister: async () => {
57304
57638
  runtime.unregisterAll();
57305
57639
  },
57306
- sendEvent: async (orbitalName, event, payload) => {
57640
+ sendEvent: async (orbitalName, event, payload, _clientId, tick, sourceTrait) => {
57307
57641
  await registrationReady;
57308
57642
  return runtime.processOrbitalEvent(orbitalName, {
57309
57643
  event,
57310
- payload
57644
+ payload,
57645
+ tick,
57646
+ sourceTrait
57311
57647
  // @almadar/runtime OrbitalEventResponse.clientEffects uses a wider ClientEffectTuple than
57312
57648
  // ServerBridge's local definition — cast at this boundary (upstream fix queued).
57313
57649
  });
@@ -59487,14 +59823,7 @@ TraitCardNode.displayName = "TraitCardNode";
59487
59823
 
59488
59824
  // components/avl/organisms/FlowCanvas.tsx
59489
59825
  init_useEventBus();
59490
- var profilerOnRender = (id, phase, actualDuration, baseDuration, _startTime, commitTime) => {
59491
- pushPerfEntry({
59492
- name: `profiler:${id}:${phase}`,
59493
- durationMs: actualDuration,
59494
- ts: commitTime,
59495
- detail: { baseDuration }
59496
- });
59497
- };
59826
+ init_perf();
59498
59827
  var flowCanvasLog = createLogger("almadar:ui:flow-canvas");
59499
59828
  var NODE_TYPES = {
59500
59829
  preview: OrbPreviewNode,