@almadar/ui 2.58.0 → 2.59.1

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.
@@ -25,6 +25,8 @@ function useUISlotManager() {
25
25
  const [slots, setSlots] = react.useState(DEFAULT_SLOTS);
26
26
  const subscribersRef = react.useRef(/* @__PURE__ */ new Set());
27
27
  const timersRef = react.useRef(/* @__PURE__ */ new Map());
28
+ const traitIndexRef = react.useRef(/* @__PURE__ */ new Map());
29
+ const traitSubscribersRef = react.useRef(/* @__PURE__ */ new Map());
28
30
  react.useEffect(() => {
29
31
  return () => {
30
32
  timersRef.current.forEach((timer) => clearTimeout(timer));
@@ -40,6 +42,29 @@ function useUISlotManager() {
40
42
  }
41
43
  });
42
44
  }, []);
45
+ const notifyTraitSubscribers = react.useCallback(
46
+ (traitName, content) => {
47
+ const subs = traitSubscribersRef.current.get(traitName);
48
+ if (!subs) return;
49
+ subs.forEach((callback) => {
50
+ try {
51
+ callback(content);
52
+ } catch (error) {
53
+ console.error(`[UISlots] Trait subscriber error (${traitName}):`, error);
54
+ }
55
+ });
56
+ },
57
+ []
58
+ );
59
+ const indexTraitRender = react.useCallback(
60
+ (traitName, content) => {
61
+ traitIndexRef.current.set(traitName, content);
62
+ },
63
+ []
64
+ );
65
+ const unindexTrait = react.useCallback((traitName) => {
66
+ traitIndexRef.current.delete(traitName);
67
+ }, []);
43
68
  const render = react.useCallback((config) => {
44
69
  const id = generateId();
45
70
  const content = {
@@ -74,11 +99,15 @@ function useUISlotManager() {
74
99
  );
75
100
  return prev;
76
101
  }
102
+ if (content.sourceTrait) {
103
+ indexTraitRender(content.sourceTrait, content);
104
+ notifyTraitSubscribers(content.sourceTrait, content);
105
+ }
77
106
  notifySubscribers(config.target, content);
78
107
  return { ...prev, [config.target]: content };
79
108
  });
80
109
  return id;
81
- }, [notifySubscribers]);
110
+ }, [notifySubscribers, notifyTraitSubscribers, indexTraitRender]);
82
111
  const clear = react.useCallback((slot) => {
83
112
  setSlots((prev) => {
84
113
  const content = prev[slot];
@@ -89,11 +118,15 @@ function useUISlotManager() {
89
118
  timersRef.current.delete(content.id);
90
119
  }
91
120
  content.onDismiss?.();
121
+ if (content.sourceTrait) {
122
+ unindexTrait(content.sourceTrait);
123
+ notifyTraitSubscribers(content.sourceTrait, null);
124
+ }
92
125
  notifySubscribers(slot, null);
93
126
  }
94
127
  return { ...prev, [slot]: null };
95
128
  });
96
- }, [notifySubscribers]);
129
+ }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
97
130
  const clearById = react.useCallback((id) => {
98
131
  setSlots((prev) => {
99
132
  const entry = Object.entries(prev).find(([, content]) => content?.id === id);
@@ -105,12 +138,16 @@ function useUISlotManager() {
105
138
  timersRef.current.delete(id);
106
139
  }
107
140
  content.onDismiss?.();
141
+ if (content.sourceTrait) {
142
+ unindexTrait(content.sourceTrait);
143
+ notifyTraitSubscribers(content.sourceTrait, null);
144
+ }
108
145
  notifySubscribers(slot, null);
109
146
  return { ...prev, [slot]: null };
110
147
  }
111
148
  return prev;
112
149
  });
113
- }, [notifySubscribers]);
150
+ }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
114
151
  const clearAll = react.useCallback(() => {
115
152
  timersRef.current.forEach((timer) => clearTimeout(timer));
116
153
  timersRef.current.clear();
@@ -118,12 +155,16 @@ function useUISlotManager() {
118
155
  Object.entries(prev).forEach(([slot, content]) => {
119
156
  if (content) {
120
157
  content.onDismiss?.();
158
+ if (content.sourceTrait) {
159
+ notifyTraitSubscribers(content.sourceTrait, null);
160
+ }
121
161
  notifySubscribers(slot, null);
122
162
  }
123
163
  });
124
164
  return DEFAULT_SLOTS;
125
165
  });
126
- }, [notifySubscribers]);
166
+ traitIndexRef.current.clear();
167
+ }, [notifySubscribers, notifyTraitSubscribers]);
127
168
  const subscribe = react.useCallback((callback) => {
128
169
  subscribersRef.current.add(callback);
129
170
  return () => {
@@ -136,6 +177,31 @@ function useUISlotManager() {
136
177
  const getContent = react.useCallback((slot) => {
137
178
  return slots[slot];
138
179
  }, [slots]);
180
+ const getTraitContent = react.useCallback(
181
+ (traitName) => {
182
+ return traitIndexRef.current.get(traitName) ?? null;
183
+ },
184
+ []
185
+ );
186
+ const subscribeTrait = react.useCallback(
187
+ (traitName, callback) => {
188
+ let set = traitSubscribersRef.current.get(traitName);
189
+ if (!set) {
190
+ set = /* @__PURE__ */ new Set();
191
+ traitSubscribersRef.current.set(traitName, set);
192
+ }
193
+ set.add(callback);
194
+ return () => {
195
+ const s = traitSubscribersRef.current.get(traitName);
196
+ if (!s) return;
197
+ s.delete(callback);
198
+ if (s.size === 0) {
199
+ traitSubscribersRef.current.delete(traitName);
200
+ }
201
+ };
202
+ },
203
+ []
204
+ );
139
205
  return {
140
206
  slots,
141
207
  render,
@@ -144,7 +210,9 @@ function useUISlotManager() {
144
210
  clearAll,
145
211
  subscribe,
146
212
  hasContent,
147
- getContent
213
+ getContent,
214
+ getTraitContent,
215
+ subscribeTrait
148
216
  };
149
217
  }
150
218
  var UISlotContext = react.createContext(null);
@@ -23,6 +23,8 @@ function useUISlotManager() {
23
23
  const [slots, setSlots] = useState(DEFAULT_SLOTS);
24
24
  const subscribersRef = useRef(/* @__PURE__ */ new Set());
25
25
  const timersRef = useRef(/* @__PURE__ */ new Map());
26
+ const traitIndexRef = useRef(/* @__PURE__ */ new Map());
27
+ const traitSubscribersRef = useRef(/* @__PURE__ */ new Map());
26
28
  useEffect(() => {
27
29
  return () => {
28
30
  timersRef.current.forEach((timer) => clearTimeout(timer));
@@ -38,6 +40,29 @@ function useUISlotManager() {
38
40
  }
39
41
  });
40
42
  }, []);
43
+ const notifyTraitSubscribers = useCallback(
44
+ (traitName, content) => {
45
+ const subs = traitSubscribersRef.current.get(traitName);
46
+ if (!subs) return;
47
+ subs.forEach((callback) => {
48
+ try {
49
+ callback(content);
50
+ } catch (error) {
51
+ console.error(`[UISlots] Trait subscriber error (${traitName}):`, error);
52
+ }
53
+ });
54
+ },
55
+ []
56
+ );
57
+ const indexTraitRender = useCallback(
58
+ (traitName, content) => {
59
+ traitIndexRef.current.set(traitName, content);
60
+ },
61
+ []
62
+ );
63
+ const unindexTrait = useCallback((traitName) => {
64
+ traitIndexRef.current.delete(traitName);
65
+ }, []);
41
66
  const render = useCallback((config) => {
42
67
  const id = generateId();
43
68
  const content = {
@@ -72,11 +97,15 @@ function useUISlotManager() {
72
97
  );
73
98
  return prev;
74
99
  }
100
+ if (content.sourceTrait) {
101
+ indexTraitRender(content.sourceTrait, content);
102
+ notifyTraitSubscribers(content.sourceTrait, content);
103
+ }
75
104
  notifySubscribers(config.target, content);
76
105
  return { ...prev, [config.target]: content };
77
106
  });
78
107
  return id;
79
- }, [notifySubscribers]);
108
+ }, [notifySubscribers, notifyTraitSubscribers, indexTraitRender]);
80
109
  const clear = useCallback((slot) => {
81
110
  setSlots((prev) => {
82
111
  const content = prev[slot];
@@ -87,11 +116,15 @@ function useUISlotManager() {
87
116
  timersRef.current.delete(content.id);
88
117
  }
89
118
  content.onDismiss?.();
119
+ if (content.sourceTrait) {
120
+ unindexTrait(content.sourceTrait);
121
+ notifyTraitSubscribers(content.sourceTrait, null);
122
+ }
90
123
  notifySubscribers(slot, null);
91
124
  }
92
125
  return { ...prev, [slot]: null };
93
126
  });
94
- }, [notifySubscribers]);
127
+ }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
95
128
  const clearById = useCallback((id) => {
96
129
  setSlots((prev) => {
97
130
  const entry = Object.entries(prev).find(([, content]) => content?.id === id);
@@ -103,12 +136,16 @@ function useUISlotManager() {
103
136
  timersRef.current.delete(id);
104
137
  }
105
138
  content.onDismiss?.();
139
+ if (content.sourceTrait) {
140
+ unindexTrait(content.sourceTrait);
141
+ notifyTraitSubscribers(content.sourceTrait, null);
142
+ }
106
143
  notifySubscribers(slot, null);
107
144
  return { ...prev, [slot]: null };
108
145
  }
109
146
  return prev;
110
147
  });
111
- }, [notifySubscribers]);
148
+ }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
112
149
  const clearAll = useCallback(() => {
113
150
  timersRef.current.forEach((timer) => clearTimeout(timer));
114
151
  timersRef.current.clear();
@@ -116,12 +153,16 @@ function useUISlotManager() {
116
153
  Object.entries(prev).forEach(([slot, content]) => {
117
154
  if (content) {
118
155
  content.onDismiss?.();
156
+ if (content.sourceTrait) {
157
+ notifyTraitSubscribers(content.sourceTrait, null);
158
+ }
119
159
  notifySubscribers(slot, null);
120
160
  }
121
161
  });
122
162
  return DEFAULT_SLOTS;
123
163
  });
124
- }, [notifySubscribers]);
164
+ traitIndexRef.current.clear();
165
+ }, [notifySubscribers, notifyTraitSubscribers]);
125
166
  const subscribe = useCallback((callback) => {
126
167
  subscribersRef.current.add(callback);
127
168
  return () => {
@@ -134,6 +175,31 @@ function useUISlotManager() {
134
175
  const getContent = useCallback((slot) => {
135
176
  return slots[slot];
136
177
  }, [slots]);
178
+ const getTraitContent = useCallback(
179
+ (traitName) => {
180
+ return traitIndexRef.current.get(traitName) ?? null;
181
+ },
182
+ []
183
+ );
184
+ const subscribeTrait = useCallback(
185
+ (traitName, callback) => {
186
+ let set = traitSubscribersRef.current.get(traitName);
187
+ if (!set) {
188
+ set = /* @__PURE__ */ new Set();
189
+ traitSubscribersRef.current.set(traitName, set);
190
+ }
191
+ set.add(callback);
192
+ return () => {
193
+ const s = traitSubscribersRef.current.get(traitName);
194
+ if (!s) return;
195
+ s.delete(callback);
196
+ if (s.size === 0) {
197
+ traitSubscribersRef.current.delete(traitName);
198
+ }
199
+ };
200
+ },
201
+ []
202
+ );
137
203
  return {
138
204
  slots,
139
205
  render,
@@ -142,7 +208,9 @@ function useUISlotManager() {
142
208
  clearAll,
143
209
  subscribe,
144
210
  hasContent,
145
- getContent
211
+ getContent,
212
+ getTraitContent,
213
+ subscribeTrait
146
214
  };
147
215
  }
148
216
  var UISlotContext = createContext(null);
@@ -1041,6 +1041,8 @@ function useUISlotManager() {
1041
1041
  const [slots, setSlots] = React.useState(DEFAULT_SLOTS);
1042
1042
  const subscribersRef = React.useRef(/* @__PURE__ */ new Set());
1043
1043
  const timersRef = React.useRef(/* @__PURE__ */ new Map());
1044
+ const traitIndexRef = React.useRef(/* @__PURE__ */ new Map());
1045
+ const traitSubscribersRef = React.useRef(/* @__PURE__ */ new Map());
1044
1046
  React.useEffect(() => {
1045
1047
  return () => {
1046
1048
  timersRef.current.forEach((timer) => clearTimeout(timer));
@@ -1056,6 +1058,29 @@ function useUISlotManager() {
1056
1058
  }
1057
1059
  });
1058
1060
  }, []);
1061
+ const notifyTraitSubscribers = React.useCallback(
1062
+ (traitName, content) => {
1063
+ const subs = traitSubscribersRef.current.get(traitName);
1064
+ if (!subs) return;
1065
+ subs.forEach((callback) => {
1066
+ try {
1067
+ callback(content);
1068
+ } catch (error) {
1069
+ console.error(`[UISlots] Trait subscriber error (${traitName}):`, error);
1070
+ }
1071
+ });
1072
+ },
1073
+ []
1074
+ );
1075
+ const indexTraitRender = React.useCallback(
1076
+ (traitName, content) => {
1077
+ traitIndexRef.current.set(traitName, content);
1078
+ },
1079
+ []
1080
+ );
1081
+ const unindexTrait = React.useCallback((traitName) => {
1082
+ traitIndexRef.current.delete(traitName);
1083
+ }, []);
1059
1084
  const render = React.useCallback((config) => {
1060
1085
  const id = generateId();
1061
1086
  const content = {
@@ -1090,11 +1115,15 @@ function useUISlotManager() {
1090
1115
  );
1091
1116
  return prev;
1092
1117
  }
1118
+ if (content.sourceTrait) {
1119
+ indexTraitRender(content.sourceTrait, content);
1120
+ notifyTraitSubscribers(content.sourceTrait, content);
1121
+ }
1093
1122
  notifySubscribers(config.target, content);
1094
1123
  return { ...prev, [config.target]: content };
1095
1124
  });
1096
1125
  return id;
1097
- }, [notifySubscribers]);
1126
+ }, [notifySubscribers, notifyTraitSubscribers, indexTraitRender]);
1098
1127
  const clear = React.useCallback((slot) => {
1099
1128
  setSlots((prev) => {
1100
1129
  const content = prev[slot];
@@ -1105,11 +1134,15 @@ function useUISlotManager() {
1105
1134
  timersRef.current.delete(content.id);
1106
1135
  }
1107
1136
  content.onDismiss?.();
1137
+ if (content.sourceTrait) {
1138
+ unindexTrait(content.sourceTrait);
1139
+ notifyTraitSubscribers(content.sourceTrait, null);
1140
+ }
1108
1141
  notifySubscribers(slot, null);
1109
1142
  }
1110
1143
  return { ...prev, [slot]: null };
1111
1144
  });
1112
- }, [notifySubscribers]);
1145
+ }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
1113
1146
  const clearById = React.useCallback((id) => {
1114
1147
  setSlots((prev) => {
1115
1148
  const entry = Object.entries(prev).find(([, content]) => content?.id === id);
@@ -1121,12 +1154,16 @@ function useUISlotManager() {
1121
1154
  timersRef.current.delete(id);
1122
1155
  }
1123
1156
  content.onDismiss?.();
1157
+ if (content.sourceTrait) {
1158
+ unindexTrait(content.sourceTrait);
1159
+ notifyTraitSubscribers(content.sourceTrait, null);
1160
+ }
1124
1161
  notifySubscribers(slot, null);
1125
1162
  return { ...prev, [slot]: null };
1126
1163
  }
1127
1164
  return prev;
1128
1165
  });
1129
- }, [notifySubscribers]);
1166
+ }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
1130
1167
  const clearAll = React.useCallback(() => {
1131
1168
  timersRef.current.forEach((timer) => clearTimeout(timer));
1132
1169
  timersRef.current.clear();
@@ -1134,12 +1171,16 @@ function useUISlotManager() {
1134
1171
  Object.entries(prev).forEach(([slot, content]) => {
1135
1172
  if (content) {
1136
1173
  content.onDismiss?.();
1174
+ if (content.sourceTrait) {
1175
+ notifyTraitSubscribers(content.sourceTrait, null);
1176
+ }
1137
1177
  notifySubscribers(slot, null);
1138
1178
  }
1139
1179
  });
1140
1180
  return DEFAULT_SLOTS;
1141
1181
  });
1142
- }, [notifySubscribers]);
1182
+ traitIndexRef.current.clear();
1183
+ }, [notifySubscribers, notifyTraitSubscribers]);
1143
1184
  const subscribe2 = React.useCallback((callback) => {
1144
1185
  subscribersRef.current.add(callback);
1145
1186
  return () => {
@@ -1152,6 +1193,31 @@ function useUISlotManager() {
1152
1193
  const getContent = React.useCallback((slot) => {
1153
1194
  return slots[slot];
1154
1195
  }, [slots]);
1196
+ const getTraitContent = React.useCallback(
1197
+ (traitName) => {
1198
+ return traitIndexRef.current.get(traitName) ?? null;
1199
+ },
1200
+ []
1201
+ );
1202
+ const subscribeTrait = React.useCallback(
1203
+ (traitName, callback) => {
1204
+ let set = traitSubscribersRef.current.get(traitName);
1205
+ if (!set) {
1206
+ set = /* @__PURE__ */ new Set();
1207
+ traitSubscribersRef.current.set(traitName, set);
1208
+ }
1209
+ set.add(callback);
1210
+ return () => {
1211
+ const s = traitSubscribersRef.current.get(traitName);
1212
+ if (!s) return;
1213
+ s.delete(callback);
1214
+ if (s.size === 0) {
1215
+ traitSubscribersRef.current.delete(traitName);
1216
+ }
1217
+ };
1218
+ },
1219
+ []
1220
+ );
1155
1221
  return {
1156
1222
  slots,
1157
1223
  render,
@@ -1160,7 +1226,9 @@ function useUISlotManager() {
1160
1226
  clearAll,
1161
1227
  subscribe: subscribe2,
1162
1228
  hasContent,
1163
- getContent
1229
+ getContent,
1230
+ getTraitContent,
1231
+ subscribeTrait
1164
1232
  };
1165
1233
  }
1166
1234
  var UI_PREFIX = "UI:";
@@ -1035,6 +1035,8 @@ function useUISlotManager() {
1035
1035
  const [slots, setSlots] = useState(DEFAULT_SLOTS);
1036
1036
  const subscribersRef = useRef(/* @__PURE__ */ new Set());
1037
1037
  const timersRef = useRef(/* @__PURE__ */ new Map());
1038
+ const traitIndexRef = useRef(/* @__PURE__ */ new Map());
1039
+ const traitSubscribersRef = useRef(/* @__PURE__ */ new Map());
1038
1040
  useEffect(() => {
1039
1041
  return () => {
1040
1042
  timersRef.current.forEach((timer) => clearTimeout(timer));
@@ -1050,6 +1052,29 @@ function useUISlotManager() {
1050
1052
  }
1051
1053
  });
1052
1054
  }, []);
1055
+ const notifyTraitSubscribers = useCallback(
1056
+ (traitName, content) => {
1057
+ const subs = traitSubscribersRef.current.get(traitName);
1058
+ if (!subs) return;
1059
+ subs.forEach((callback) => {
1060
+ try {
1061
+ callback(content);
1062
+ } catch (error) {
1063
+ console.error(`[UISlots] Trait subscriber error (${traitName}):`, error);
1064
+ }
1065
+ });
1066
+ },
1067
+ []
1068
+ );
1069
+ const indexTraitRender = useCallback(
1070
+ (traitName, content) => {
1071
+ traitIndexRef.current.set(traitName, content);
1072
+ },
1073
+ []
1074
+ );
1075
+ const unindexTrait = useCallback((traitName) => {
1076
+ traitIndexRef.current.delete(traitName);
1077
+ }, []);
1053
1078
  const render = useCallback((config) => {
1054
1079
  const id = generateId();
1055
1080
  const content = {
@@ -1084,11 +1109,15 @@ function useUISlotManager() {
1084
1109
  );
1085
1110
  return prev;
1086
1111
  }
1112
+ if (content.sourceTrait) {
1113
+ indexTraitRender(content.sourceTrait, content);
1114
+ notifyTraitSubscribers(content.sourceTrait, content);
1115
+ }
1087
1116
  notifySubscribers(config.target, content);
1088
1117
  return { ...prev, [config.target]: content };
1089
1118
  });
1090
1119
  return id;
1091
- }, [notifySubscribers]);
1120
+ }, [notifySubscribers, notifyTraitSubscribers, indexTraitRender]);
1092
1121
  const clear = useCallback((slot) => {
1093
1122
  setSlots((prev) => {
1094
1123
  const content = prev[slot];
@@ -1099,11 +1128,15 @@ function useUISlotManager() {
1099
1128
  timersRef.current.delete(content.id);
1100
1129
  }
1101
1130
  content.onDismiss?.();
1131
+ if (content.sourceTrait) {
1132
+ unindexTrait(content.sourceTrait);
1133
+ notifyTraitSubscribers(content.sourceTrait, null);
1134
+ }
1102
1135
  notifySubscribers(slot, null);
1103
1136
  }
1104
1137
  return { ...prev, [slot]: null };
1105
1138
  });
1106
- }, [notifySubscribers]);
1139
+ }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
1107
1140
  const clearById = useCallback((id) => {
1108
1141
  setSlots((prev) => {
1109
1142
  const entry = Object.entries(prev).find(([, content]) => content?.id === id);
@@ -1115,12 +1148,16 @@ function useUISlotManager() {
1115
1148
  timersRef.current.delete(id);
1116
1149
  }
1117
1150
  content.onDismiss?.();
1151
+ if (content.sourceTrait) {
1152
+ unindexTrait(content.sourceTrait);
1153
+ notifyTraitSubscribers(content.sourceTrait, null);
1154
+ }
1118
1155
  notifySubscribers(slot, null);
1119
1156
  return { ...prev, [slot]: null };
1120
1157
  }
1121
1158
  return prev;
1122
1159
  });
1123
- }, [notifySubscribers]);
1160
+ }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
1124
1161
  const clearAll = useCallback(() => {
1125
1162
  timersRef.current.forEach((timer) => clearTimeout(timer));
1126
1163
  timersRef.current.clear();
@@ -1128,12 +1165,16 @@ function useUISlotManager() {
1128
1165
  Object.entries(prev).forEach(([slot, content]) => {
1129
1166
  if (content) {
1130
1167
  content.onDismiss?.();
1168
+ if (content.sourceTrait) {
1169
+ notifyTraitSubscribers(content.sourceTrait, null);
1170
+ }
1131
1171
  notifySubscribers(slot, null);
1132
1172
  }
1133
1173
  });
1134
1174
  return DEFAULT_SLOTS;
1135
1175
  });
1136
- }, [notifySubscribers]);
1176
+ traitIndexRef.current.clear();
1177
+ }, [notifySubscribers, notifyTraitSubscribers]);
1137
1178
  const subscribe2 = useCallback((callback) => {
1138
1179
  subscribersRef.current.add(callback);
1139
1180
  return () => {
@@ -1146,6 +1187,31 @@ function useUISlotManager() {
1146
1187
  const getContent = useCallback((slot) => {
1147
1188
  return slots[slot];
1148
1189
  }, [slots]);
1190
+ const getTraitContent = useCallback(
1191
+ (traitName) => {
1192
+ return traitIndexRef.current.get(traitName) ?? null;
1193
+ },
1194
+ []
1195
+ );
1196
+ const subscribeTrait = useCallback(
1197
+ (traitName, callback) => {
1198
+ let set = traitSubscribersRef.current.get(traitName);
1199
+ if (!set) {
1200
+ set = /* @__PURE__ */ new Set();
1201
+ traitSubscribersRef.current.set(traitName, set);
1202
+ }
1203
+ set.add(callback);
1204
+ return () => {
1205
+ const s = traitSubscribersRef.current.get(traitName);
1206
+ if (!s) return;
1207
+ s.delete(callback);
1208
+ if (s.size === 0) {
1209
+ traitSubscribersRef.current.delete(traitName);
1210
+ }
1211
+ };
1212
+ },
1213
+ []
1214
+ );
1149
1215
  return {
1150
1216
  slots,
1151
1217
  render,
@@ -1154,7 +1220,9 @@ function useUISlotManager() {
1154
1220
  clearAll,
1155
1221
  subscribe: subscribe2,
1156
1222
  hasContent,
1157
- getContent
1223
+ getContent,
1224
+ getTraitContent,
1225
+ subscribeTrait
1158
1226
  };
1159
1227
  }
1160
1228
  var UI_PREFIX = "UI:";
@@ -54,6 +54,16 @@ export interface RenderUIConfig {
54
54
  * Callback for slot changes
55
55
  */
56
56
  export type SlotChangeCallback = (slot: UISlot, content: SlotContent | null) => void;
57
+ /**
58
+ * Fires when a specific trait's current render-ui output changes.
59
+ * Used by `<TraitFrame>` to subscribe only to the trait it embeds,
60
+ * avoiding wide re-renders when unrelated slots update.
61
+ *
62
+ * Per-trait index stores exactly one entry per trait (the most recent
63
+ * render), so the callback signature carries just the new content —
64
+ * no per-slot disambiguation needed.
65
+ */
66
+ export type TraitChangeCallback = (content: SlotContent | null) => void;
57
67
  /**
58
68
  * UI Slot Manager interface
59
69
  */
@@ -74,6 +84,24 @@ export interface UISlotManager {
74
84
  hasContent: (slot: UISlot) => boolean;
75
85
  /** Get content for a slot */
76
86
  getContent: (slot: UISlot) => SlotContent | null;
87
+ /**
88
+ * Look up the most recent `render-ui` output a given trait produced,
89
+ * keyed by `SlotContent.sourceTrait`. Returns `null` when the trait
90
+ * hasn't rendered yet.
91
+ *
92
+ * Backs the `@trait.X` binding's client-side resolution via
93
+ * `<TraitFrame>`. See `docs/Almadar_Std_Gaps.md` §3.8. The binding is
94
+ * single-segment — there's intentionally no slot disambiguation.
95
+ */
96
+ getTraitContent: (traitName: string) => SlotContent | null;
97
+ /**
98
+ * Subscribe to changes in a specific trait's render output. Returns
99
+ * an unsubscribe function.
100
+ *
101
+ * Scoped per-trait so `<TraitFrame>` components only re-render when
102
+ * the trait they embed actually changes — not on every slot update.
103
+ */
104
+ subscribeTrait: (traitName: string, callback: TraitChangeCallback) => () => void;
77
105
  }
78
106
  declare const DEFAULT_SLOTS: Record<UISlot, SlotContent | null>;
79
107
  /**