@almadar/ui 4.0.1 → 4.1.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.
@@ -1014,30 +1014,76 @@ function useEmitEvent() {
1014
1014
  [eventBus]
1015
1015
  );
1016
1016
  }
1017
- var DEFAULT_SLOTS = {
1018
- main: null,
1019
- sidebar: null,
1020
- modal: null,
1021
- drawer: null,
1022
- overlay: null,
1023
- center: null,
1024
- toast: null,
1025
- "hud-top": null,
1026
- "hud-bottom": null,
1027
- "hud-left": null,
1028
- "hud-right": null,
1029
- floating: null
1030
- };
1017
+ var DEFAULT_SOURCE_KEY = "__default__";
1018
+ var MULTI_SOURCE_STACK_TRAIT = "__multi_source_stack__";
1019
+ var ALL_SLOTS = [
1020
+ "main",
1021
+ "sidebar",
1022
+ "modal",
1023
+ "drawer",
1024
+ "overlay",
1025
+ "center",
1026
+ "toast",
1027
+ "hud-top",
1028
+ "hud-bottom",
1029
+ "hud-left",
1030
+ "hud-right",
1031
+ "floating"
1032
+ ];
1033
+ var DEFAULT_SLOTS = ALL_SLOTS.reduce(
1034
+ (acc, slot) => {
1035
+ acc[slot] = null;
1036
+ return acc;
1037
+ },
1038
+ {}
1039
+ );
1040
+ var DEFAULT_SOURCES = ALL_SLOTS.reduce(
1041
+ (acc, slot) => {
1042
+ acc[slot] = {};
1043
+ return acc;
1044
+ },
1045
+ {}
1046
+ );
1031
1047
  var idCounter = 0;
1032
1048
  function generateId() {
1033
1049
  return `slot-content-${++idCounter}-${Date.now()}`;
1034
1050
  }
1051
+ function aggregateSlot(sources) {
1052
+ if (!sources) return null;
1053
+ const entries = Object.entries(sources);
1054
+ if (entries.length === 0) return null;
1055
+ if (entries.length === 1) return entries[0][1];
1056
+ const children = entries.map(([, entry]) => ({
1057
+ type: entry.pattern,
1058
+ ...entry.props
1059
+ }));
1060
+ const stackId = `slot-content-stack-${entries.map(([k]) => k).join("-")}`;
1061
+ return {
1062
+ id: stackId,
1063
+ pattern: "stack",
1064
+ props: {
1065
+ direction: "vertical",
1066
+ gap: "lg",
1067
+ children
1068
+ },
1069
+ priority: 0,
1070
+ animation: "fade",
1071
+ sourceTrait: MULTI_SOURCE_STACK_TRAIT
1072
+ };
1073
+ }
1035
1074
  function useUISlotManager() {
1036
- const [slots, setSlots] = useState(DEFAULT_SLOTS);
1075
+ const [sources, setSources] = useState(DEFAULT_SOURCES);
1037
1076
  const subscribersRef = useRef(/* @__PURE__ */ new Set());
1038
1077
  const timersRef = useRef(/* @__PURE__ */ new Map());
1039
1078
  const traitIndexRef = useRef(/* @__PURE__ */ new Map());
1040
1079
  const traitSubscribersRef = useRef(/* @__PURE__ */ new Map());
1080
+ const slots = useMemo(() => {
1081
+ const out = { ...DEFAULT_SLOTS };
1082
+ for (const slot of ALL_SLOTS) {
1083
+ out[slot] = aggregateSlot(sources[slot]);
1084
+ }
1085
+ return out;
1086
+ }, [sources]);
1041
1087
  useEffect(() => {
1042
1088
  return () => {
1043
1089
  timersRef.current.forEach((timer) => clearTimeout(timer));
@@ -1076,103 +1122,160 @@ function useUISlotManager() {
1076
1122
  const unindexTrait = useCallback((traitName) => {
1077
1123
  traitIndexRef.current.delete(traitName);
1078
1124
  }, []);
1079
- const render = useCallback((config) => {
1080
- const id = generateId();
1081
- const content = {
1082
- id,
1083
- pattern: config.pattern,
1084
- props: config.props ?? {},
1085
- priority: config.priority ?? 0,
1086
- animation: config.animation ?? "fade",
1087
- onDismiss: config.onDismiss,
1088
- sourceTrait: config.sourceTrait
1089
- };
1090
- if (config.autoDismissMs && config.autoDismissMs > 0) {
1091
- content.autoDismissAt = Date.now() + config.autoDismissMs;
1092
- const timer = setTimeout(() => {
1093
- setSlots((prev) => {
1094
- if (prev[config.target]?.id === id) {
1095
- content.onDismiss?.();
1096
- notifySubscribers(config.target, null);
1097
- return { ...prev, [config.target]: null };
1098
- }
1099
- return prev;
1100
- });
1101
- timersRef.current.delete(id);
1102
- }, config.autoDismissMs);
1103
- timersRef.current.set(id, timer);
1104
- }
1105
- setSlots((prev) => {
1106
- const existing = prev[config.target];
1107
- if (existing && existing.priority > content.priority) {
1108
- console.warn(
1109
- `[UISlots] Slot "${config.target}" already has higher priority content (${existing.priority} > ${content.priority})`
1110
- );
1111
- return prev;
1112
- }
1113
- if (content.sourceTrait) {
1114
- indexTraitRender(content.sourceTrait, content);
1115
- notifyTraitSubscribers(content.sourceTrait, content);
1125
+ const render = useCallback(
1126
+ (config) => {
1127
+ const id = generateId();
1128
+ const sourceKey = config.sourceTrait ?? DEFAULT_SOURCE_KEY;
1129
+ const content = {
1130
+ id,
1131
+ pattern: config.pattern,
1132
+ props: config.props ?? {},
1133
+ priority: config.priority ?? 0,
1134
+ animation: config.animation ?? "fade",
1135
+ onDismiss: config.onDismiss,
1136
+ sourceTrait: config.sourceTrait
1137
+ };
1138
+ if (config.autoDismissMs && config.autoDismissMs > 0) {
1139
+ content.autoDismissAt = Date.now() + config.autoDismissMs;
1140
+ const timer = setTimeout(() => {
1141
+ setSources((prev) => {
1142
+ const slotSources = prev[config.target];
1143
+ if (slotSources && slotSources[sourceKey]?.id === id) {
1144
+ content.onDismiss?.();
1145
+ const next = { ...slotSources };
1146
+ delete next[sourceKey];
1147
+ const updated = { ...prev, [config.target]: next };
1148
+ notifySubscribers(config.target, aggregateSlot(next));
1149
+ return updated;
1150
+ }
1151
+ return prev;
1152
+ });
1153
+ timersRef.current.delete(id);
1154
+ }, config.autoDismissMs);
1155
+ timersRef.current.set(id, timer);
1116
1156
  }
1117
- notifySubscribers(config.target, content);
1118
- return { ...prev, [config.target]: content };
1119
- });
1120
- return id;
1121
- }, [notifySubscribers, notifyTraitSubscribers, indexTraitRender]);
1122
- const clear = useCallback((slot) => {
1123
- setSlots((prev) => {
1124
- const content = prev[slot];
1125
- if (content) {
1126
- const timer = timersRef.current.get(content.id);
1127
- if (timer) {
1128
- clearTimeout(timer);
1129
- timersRef.current.delete(content.id);
1157
+ setSources((prev) => {
1158
+ const slotSources = prev[config.target] ?? {};
1159
+ const existing = slotSources[sourceKey];
1160
+ if (existing && existing.priority > content.priority) {
1161
+ console.warn(
1162
+ `[UISlots] Slot "${config.target}" source "${sourceKey}" already has higher priority content (${existing.priority} > ${content.priority})`
1163
+ );
1164
+ return prev;
1130
1165
  }
1131
- content.onDismiss?.();
1166
+ const nextSources = {
1167
+ ...slotSources,
1168
+ [sourceKey]: content
1169
+ };
1170
+ const nextAll = { ...prev, [config.target]: nextSources };
1132
1171
  if (content.sourceTrait) {
1133
- unindexTrait(content.sourceTrait);
1134
- notifyTraitSubscribers(content.sourceTrait, null);
1172
+ indexTraitRender(content.sourceTrait, content);
1173
+ notifyTraitSubscribers(content.sourceTrait, content);
1174
+ }
1175
+ notifySubscribers(config.target, aggregateSlot(nextSources));
1176
+ return nextAll;
1177
+ });
1178
+ return id;
1179
+ },
1180
+ [notifySubscribers, notifyTraitSubscribers, indexTraitRender]
1181
+ );
1182
+ const clear = useCallback(
1183
+ (slot) => {
1184
+ setSources((prev) => {
1185
+ const slotSources = prev[slot];
1186
+ if (!slotSources || Object.keys(slotSources).length === 0) {
1187
+ return prev;
1188
+ }
1189
+ for (const content of Object.values(slotSources)) {
1190
+ const timer = timersRef.current.get(content.id);
1191
+ if (timer) {
1192
+ clearTimeout(timer);
1193
+ timersRef.current.delete(content.id);
1194
+ }
1195
+ content.onDismiss?.();
1196
+ if (content.sourceTrait) {
1197
+ unindexTrait(content.sourceTrait);
1198
+ notifyTraitSubscribers(content.sourceTrait, null);
1199
+ }
1135
1200
  }
1136
1201
  notifySubscribers(slot, null);
1137
- }
1138
- return { ...prev, [slot]: null };
1139
- });
1140
- }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
1141
- const clearById = useCallback((id) => {
1142
- setSlots((prev) => {
1143
- const entry = Object.entries(prev).find(([, content]) => content?.id === id);
1144
- if (entry) {
1145
- const [slot, content] = entry;
1146
- const timer = timersRef.current.get(id);
1202
+ return { ...prev, [slot]: {} };
1203
+ });
1204
+ },
1205
+ [notifySubscribers, notifyTraitSubscribers, unindexTrait]
1206
+ );
1207
+ const clearBySource = useCallback(
1208
+ (slot, sourceTrait) => {
1209
+ const sourceKey = sourceTrait;
1210
+ setSources((prev) => {
1211
+ const slotSources = prev[slot];
1212
+ if (!slotSources || !(sourceKey in slotSources)) return prev;
1213
+ const content = slotSources[sourceKey];
1214
+ const timer = timersRef.current.get(content.id);
1147
1215
  if (timer) {
1148
1216
  clearTimeout(timer);
1149
- timersRef.current.delete(id);
1217
+ timersRef.current.delete(content.id);
1150
1218
  }
1151
1219
  content.onDismiss?.();
1152
1220
  if (content.sourceTrait) {
1153
1221
  unindexTrait(content.sourceTrait);
1154
1222
  notifyTraitSubscribers(content.sourceTrait, null);
1155
1223
  }
1156
- notifySubscribers(slot, null);
1157
- return { ...prev, [slot]: null };
1158
- }
1159
- return prev;
1160
- });
1161
- }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
1224
+ const nextSources = { ...slotSources };
1225
+ delete nextSources[sourceKey];
1226
+ notifySubscribers(slot, aggregateSlot(nextSources));
1227
+ return { ...prev, [slot]: nextSources };
1228
+ });
1229
+ },
1230
+ [notifySubscribers, notifyTraitSubscribers, unindexTrait]
1231
+ );
1232
+ const clearById = useCallback(
1233
+ (id) => {
1234
+ setSources((prev) => {
1235
+ for (const slot of ALL_SLOTS) {
1236
+ const slotSources = prev[slot];
1237
+ if (!slotSources) continue;
1238
+ const matchKey = Object.keys(slotSources).find(
1239
+ (k) => slotSources[k].id === id
1240
+ );
1241
+ if (!matchKey) continue;
1242
+ const content = slotSources[matchKey];
1243
+ const timer = timersRef.current.get(id);
1244
+ if (timer) {
1245
+ clearTimeout(timer);
1246
+ timersRef.current.delete(id);
1247
+ }
1248
+ content.onDismiss?.();
1249
+ if (content.sourceTrait) {
1250
+ unindexTrait(content.sourceTrait);
1251
+ notifyTraitSubscribers(content.sourceTrait, null);
1252
+ }
1253
+ const nextSources = { ...slotSources };
1254
+ delete nextSources[matchKey];
1255
+ notifySubscribers(slot, aggregateSlot(nextSources));
1256
+ return { ...prev, [slot]: nextSources };
1257
+ }
1258
+ return prev;
1259
+ });
1260
+ },
1261
+ [notifySubscribers, notifyTraitSubscribers, unindexTrait]
1262
+ );
1162
1263
  const clearAll = useCallback(() => {
1163
1264
  timersRef.current.forEach((timer) => clearTimeout(timer));
1164
1265
  timersRef.current.clear();
1165
- setSlots((prev) => {
1166
- Object.entries(prev).forEach(([slot, content]) => {
1167
- if (content) {
1266
+ setSources((prev) => {
1267
+ for (const slot of ALL_SLOTS) {
1268
+ const slotSources = prev[slot];
1269
+ if (!slotSources) continue;
1270
+ for (const content of Object.values(slotSources)) {
1168
1271
  content.onDismiss?.();
1169
1272
  if (content.sourceTrait) {
1170
1273
  notifyTraitSubscribers(content.sourceTrait, null);
1171
1274
  }
1172
- notifySubscribers(slot, null);
1173
1275
  }
1174
- });
1175
- return DEFAULT_SLOTS;
1276
+ notifySubscribers(slot, null);
1277
+ }
1278
+ return DEFAULT_SOURCES;
1176
1279
  });
1177
1280
  traitIndexRef.current.clear();
1178
1281
  }, [notifySubscribers, notifyTraitSubscribers]);
@@ -1182,16 +1285,16 @@ function useUISlotManager() {
1182
1285
  subscribersRef.current.delete(callback);
1183
1286
  };
1184
1287
  }, []);
1185
- const hasContent = useCallback((slot) => {
1186
- return slots[slot] !== null;
1187
- }, [slots]);
1188
- const getContent = useCallback((slot) => {
1189
- return slots[slot];
1190
- }, [slots]);
1288
+ const hasContent = useCallback(
1289
+ (slot) => slots[slot] !== null,
1290
+ [slots]
1291
+ );
1292
+ const getContent = useCallback(
1293
+ (slot) => slots[slot],
1294
+ [slots]
1295
+ );
1191
1296
  const getTraitContent = useCallback(
1192
- (traitName) => {
1193
- return traitIndexRef.current.get(traitName) ?? null;
1194
- },
1297
+ (traitName) => traitIndexRef.current.get(traitName) ?? null,
1195
1298
  []
1196
1299
  );
1197
1300
  const subscribeTrait = useCallback(
@@ -1217,6 +1320,7 @@ function useUISlotManager() {
1217
1320
  slots,
1218
1321
  render,
1219
1322
  clear,
1323
+ clearBySource,
1220
1324
  clearById,
1221
1325
  clearAll,
1222
1326
  subscribe: subscribe2,
@@ -1,3 +1,4 @@
1
+ import type { EventPayload } from '@almadar/core';
1
2
  /**
2
3
  * Valid UI slot names
3
4
  */
@@ -6,6 +7,14 @@ export type UISlot = 'main' | 'sidebar' | 'modal' | 'drawer' | 'overlay' | 'cent
6
7
  * Animation types for slot transitions
7
8
  */
8
9
  export type SlotAnimation = 'fade' | 'slide' | 'scale' | 'none';
10
+ /**
11
+ * Pattern-specific props carried by a rendered slot. Pattern authors decide
12
+ * the concrete shape; the slot manager treats this as an opaque record of
13
+ * field-like values sourced from the event-bus payload vocabulary so the
14
+ * same types round-trip through render-ui → useUISlots → UISlotRenderer
15
+ * without a private re-coercion boundary.
16
+ */
17
+ export type SlotProps = Record<string, EventPayload[string] | unknown>;
9
18
  /**
10
19
  * Content rendered in a slot
11
20
  */
@@ -15,7 +24,7 @@ export interface SlotContent {
15
24
  /** Pattern/component type to render */
16
25
  pattern: string;
17
26
  /** Props to pass to the pattern component */
18
- props: Record<string, unknown>;
27
+ props: SlotProps;
19
28
  /** Priority for conflict resolution (higher wins) */
20
29
  priority: number;
21
30
  /** Animation for showing/hiding */
@@ -38,7 +47,7 @@ export interface RenderUIConfig {
38
47
  /** Pattern/component to render */
39
48
  pattern: string;
40
49
  /** Props for the pattern */
41
- props?: Record<string, unknown>;
50
+ props?: SlotProps;
42
51
  /** Priority (default: 0) */
43
52
  priority?: number;
44
53
  /** Animation type */
@@ -68,12 +77,20 @@ export type TraitChangeCallback = (content: SlotContent | null) => void;
68
77
  * UI Slot Manager interface
69
78
  */
70
79
  export interface UISlotManager {
71
- /** Current content for each slot */
80
+ /**
81
+ * Current aggregate content for each slot. Single-source slots hold
82
+ * that source's SlotContent; multi-source slots hold a synthetic
83
+ * `stack` wrapper whose `children` prop is the list of per-source
84
+ * pattern configs in insertion order. Single consumers can continue
85
+ * to read `slots[slot]` without knowing the slot is multi-source.
86
+ */
72
87
  slots: Record<UISlot, SlotContent | null>;
73
88
  /** Render content to a slot */
74
89
  render: (config: RenderUIConfig) => string;
75
- /** Clear a specific slot */
90
+ /** Clear all source entries from a slot */
76
91
  clear: (slot: UISlot) => void;
92
+ /** Clear a single source's contribution from a slot, keeping others */
93
+ clearBySource: (slot: UISlot, sourceTrait: string) => void;
77
94
  /** Clear content by ID */
78
95
  clearById: (id: string) => void;
79
96
  /** Clear all slots */
@@ -82,7 +99,7 @@ export interface UISlotManager {
82
99
  subscribe: (callback: SlotChangeCallback) => () => void;
83
100
  /** Check if a slot has content */
84
101
  hasContent: (slot: UISlot) => boolean;
85
- /** Get content for a slot */
102
+ /** Get aggregated content for a slot (single or synthetic stack) */
86
103
  getContent: (slot: UISlot) => SlotContent | null;
87
104
  /**
88
105
  * Look up the most recent `render-ui` output a given trait produced,
@@ -37828,6 +37828,34 @@ var init_UISlotRenderer = __esm({
37828
37828
 
37829
37829
  // hooks/index.ts
37830
37830
  init_useEventBus();
37831
+ var ALL_SLOTS = [
37832
+ "main",
37833
+ "sidebar",
37834
+ "modal",
37835
+ "drawer",
37836
+ "overlay",
37837
+ "center",
37838
+ "toast",
37839
+ "hud-top",
37840
+ "hud-bottom",
37841
+ "hud-left",
37842
+ "hud-right",
37843
+ "floating"
37844
+ ];
37845
+ ALL_SLOTS.reduce(
37846
+ (acc, slot) => {
37847
+ acc[slot] = null;
37848
+ return acc;
37849
+ },
37850
+ {}
37851
+ );
37852
+ ALL_SLOTS.reduce(
37853
+ (acc, slot) => {
37854
+ acc[slot] = {};
37855
+ return acc;
37856
+ },
37857
+ {}
37858
+ );
37831
37859
 
37832
37860
  // hooks/useUIEvents.ts
37833
37861
  init_useEventBus();
@@ -38870,13 +38898,11 @@ function SlotBridge() {
38870
38898
  return null;
38871
38899
  }
38872
38900
  function applyServerEffects(effects, uiSlots, onNavigate) {
38873
- const perSlotRenders = /* @__PURE__ */ new Map();
38874
38901
  for (const eff of effects) {
38875
38902
  if (eff.type === "render-ui" && eff.slot && eff.pattern) {
38876
38903
  const patternRecord = eff.pattern;
38877
38904
  const { type: patternType, children, ...inlineProps } = patternRecord;
38878
38905
  const normalizedChildren = Array.isArray(children) ? children.map((c) => normalizeChild(c)) : children;
38879
- const sourceTrait = eff.traitName ?? "server";
38880
38906
  uiSlots.render({
38881
38907
  target: eff.slot,
38882
38908
  pattern: patternType,
@@ -38884,38 +38910,12 @@ function applyServerEffects(effects, uiSlots, onNavigate) {
38884
38910
  ...inlineProps,
38885
38911
  ...normalizedChildren !== void 0 ? { children: normalizedChildren } : {}
38886
38912
  },
38887
- sourceTrait
38913
+ sourceTrait: eff.traitName ?? "server"
38888
38914
  });
38889
- const bucket = perSlotRenders.get(eff.slot) ?? [];
38890
- bucket.push({
38891
- sourceTrait,
38892
- pattern: {
38893
- type: patternType,
38894
- ...inlineProps,
38895
- ...normalizedChildren !== void 0 ? { children: normalizedChildren } : {}
38896
- }
38897
- });
38898
- perSlotRenders.set(eff.slot, bucket);
38899
38915
  } else if (eff.type === "navigate" && eff.route && onNavigate) {
38900
38916
  onNavigate(eff.route, eff.params);
38901
38917
  }
38902
38918
  }
38903
- for (const [slot, bucket] of perSlotRenders) {
38904
- const distinctSources = new Set(bucket.map((b) => b.sourceTrait));
38905
- if (distinctSources.size <= 1) continue;
38906
- uiSlots.render({
38907
- target: slot,
38908
- pattern: "stack",
38909
- props: {
38910
- direction: "vertical",
38911
- gap: "lg",
38912
- children: bucket.map((b) => b.pattern)
38913
- },
38914
- // Use a synthetic wrapper source trait; individual traits' frames
38915
- // already live in the per-trait index from the per-effect loop.
38916
- sourceTrait: "__multi_source_stack__"
38917
- });
38918
- }
38919
38919
  }
38920
38920
  function TraitInitializer({ traits: traits2, orbitalNames, onNavigate, onLocalFallback, persistence }) {
38921
38921
  const slotsActions = useSlotsActions();
@@ -37783,6 +37783,34 @@ var init_UISlotRenderer = __esm({
37783
37783
 
37784
37784
  // hooks/index.ts
37785
37785
  init_useEventBus();
37786
+ var ALL_SLOTS = [
37787
+ "main",
37788
+ "sidebar",
37789
+ "modal",
37790
+ "drawer",
37791
+ "overlay",
37792
+ "center",
37793
+ "toast",
37794
+ "hud-top",
37795
+ "hud-bottom",
37796
+ "hud-left",
37797
+ "hud-right",
37798
+ "floating"
37799
+ ];
37800
+ ALL_SLOTS.reduce(
37801
+ (acc, slot) => {
37802
+ acc[slot] = null;
37803
+ return acc;
37804
+ },
37805
+ {}
37806
+ );
37807
+ ALL_SLOTS.reduce(
37808
+ (acc, slot) => {
37809
+ acc[slot] = {};
37810
+ return acc;
37811
+ },
37812
+ {}
37813
+ );
37786
37814
 
37787
37815
  // hooks/useUIEvents.ts
37788
37816
  init_useEventBus();
@@ -38825,13 +38853,11 @@ function SlotBridge() {
38825
38853
  return null;
38826
38854
  }
38827
38855
  function applyServerEffects(effects, uiSlots, onNavigate) {
38828
- const perSlotRenders = /* @__PURE__ */ new Map();
38829
38856
  for (const eff of effects) {
38830
38857
  if (eff.type === "render-ui" && eff.slot && eff.pattern) {
38831
38858
  const patternRecord = eff.pattern;
38832
38859
  const { type: patternType, children, ...inlineProps } = patternRecord;
38833
38860
  const normalizedChildren = Array.isArray(children) ? children.map((c) => normalizeChild(c)) : children;
38834
- const sourceTrait = eff.traitName ?? "server";
38835
38861
  uiSlots.render({
38836
38862
  target: eff.slot,
38837
38863
  pattern: patternType,
@@ -38839,38 +38865,12 @@ function applyServerEffects(effects, uiSlots, onNavigate) {
38839
38865
  ...inlineProps,
38840
38866
  ...normalizedChildren !== void 0 ? { children: normalizedChildren } : {}
38841
38867
  },
38842
- sourceTrait
38868
+ sourceTrait: eff.traitName ?? "server"
38843
38869
  });
38844
- const bucket = perSlotRenders.get(eff.slot) ?? [];
38845
- bucket.push({
38846
- sourceTrait,
38847
- pattern: {
38848
- type: patternType,
38849
- ...inlineProps,
38850
- ...normalizedChildren !== void 0 ? { children: normalizedChildren } : {}
38851
- }
38852
- });
38853
- perSlotRenders.set(eff.slot, bucket);
38854
38870
  } else if (eff.type === "navigate" && eff.route && onNavigate) {
38855
38871
  onNavigate(eff.route, eff.params);
38856
38872
  }
38857
38873
  }
38858
- for (const [slot, bucket] of perSlotRenders) {
38859
- const distinctSources = new Set(bucket.map((b) => b.sourceTrait));
38860
- if (distinctSources.size <= 1) continue;
38861
- uiSlots.render({
38862
- target: slot,
38863
- pattern: "stack",
38864
- props: {
38865
- direction: "vertical",
38866
- gap: "lg",
38867
- children: bucket.map((b) => b.pattern)
38868
- },
38869
- // Use a synthetic wrapper source trait; individual traits' frames
38870
- // already live in the per-trait index from the per-effect loop.
38871
- sourceTrait: "__multi_source_stack__"
38872
- });
38873
- }
38874
38874
  }
38875
38875
  function TraitInitializer({ traits: traits2, orbitalNames, onNavigate, onLocalFallback, persistence }) {
38876
38876
  const slotsActions = useSlotsActions();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "main": "./dist/components/index.js",