@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.
@@ -1016,30 +1016,76 @@ function useEmitEvent() {
1016
1016
  [eventBus]
1017
1017
  );
1018
1018
  }
1019
- var DEFAULT_SLOTS = {
1020
- main: null,
1021
- sidebar: null,
1022
- modal: null,
1023
- drawer: null,
1024
- overlay: null,
1025
- center: null,
1026
- toast: null,
1027
- "hud-top": null,
1028
- "hud-bottom": null,
1029
- "hud-left": null,
1030
- "hud-right": null,
1031
- floating: null
1032
- };
1019
+ var DEFAULT_SOURCE_KEY = "__default__";
1020
+ var MULTI_SOURCE_STACK_TRAIT = "__multi_source_stack__";
1021
+ var ALL_SLOTS = [
1022
+ "main",
1023
+ "sidebar",
1024
+ "modal",
1025
+ "drawer",
1026
+ "overlay",
1027
+ "center",
1028
+ "toast",
1029
+ "hud-top",
1030
+ "hud-bottom",
1031
+ "hud-left",
1032
+ "hud-right",
1033
+ "floating"
1034
+ ];
1035
+ var DEFAULT_SLOTS = ALL_SLOTS.reduce(
1036
+ (acc, slot) => {
1037
+ acc[slot] = null;
1038
+ return acc;
1039
+ },
1040
+ {}
1041
+ );
1042
+ var DEFAULT_SOURCES = ALL_SLOTS.reduce(
1043
+ (acc, slot) => {
1044
+ acc[slot] = {};
1045
+ return acc;
1046
+ },
1047
+ {}
1048
+ );
1033
1049
  var idCounter = 0;
1034
1050
  function generateId() {
1035
1051
  return `slot-content-${++idCounter}-${Date.now()}`;
1036
1052
  }
1053
+ function aggregateSlot(sources) {
1054
+ if (!sources) return null;
1055
+ const entries = Object.entries(sources);
1056
+ if (entries.length === 0) return null;
1057
+ if (entries.length === 1) return entries[0][1];
1058
+ const children = entries.map(([, entry]) => ({
1059
+ type: entry.pattern,
1060
+ ...entry.props
1061
+ }));
1062
+ const stackId = `slot-content-stack-${entries.map(([k]) => k).join("-")}`;
1063
+ return {
1064
+ id: stackId,
1065
+ pattern: "stack",
1066
+ props: {
1067
+ direction: "vertical",
1068
+ gap: "lg",
1069
+ children
1070
+ },
1071
+ priority: 0,
1072
+ animation: "fade",
1073
+ sourceTrait: MULTI_SOURCE_STACK_TRAIT
1074
+ };
1075
+ }
1037
1076
  function useUISlotManager() {
1038
- const [slots, setSlots] = react.useState(DEFAULT_SLOTS);
1077
+ const [sources, setSources] = react.useState(DEFAULT_SOURCES);
1039
1078
  const subscribersRef = react.useRef(/* @__PURE__ */ new Set());
1040
1079
  const timersRef = react.useRef(/* @__PURE__ */ new Map());
1041
1080
  const traitIndexRef = react.useRef(/* @__PURE__ */ new Map());
1042
1081
  const traitSubscribersRef = react.useRef(/* @__PURE__ */ new Map());
1082
+ const slots = react.useMemo(() => {
1083
+ const out = { ...DEFAULT_SLOTS };
1084
+ for (const slot of ALL_SLOTS) {
1085
+ out[slot] = aggregateSlot(sources[slot]);
1086
+ }
1087
+ return out;
1088
+ }, [sources]);
1043
1089
  react.useEffect(() => {
1044
1090
  return () => {
1045
1091
  timersRef.current.forEach((timer) => clearTimeout(timer));
@@ -1078,103 +1124,160 @@ function useUISlotManager() {
1078
1124
  const unindexTrait = react.useCallback((traitName) => {
1079
1125
  traitIndexRef.current.delete(traitName);
1080
1126
  }, []);
1081
- const render = react.useCallback((config) => {
1082
- const id = generateId();
1083
- const content = {
1084
- id,
1085
- pattern: config.pattern,
1086
- props: config.props ?? {},
1087
- priority: config.priority ?? 0,
1088
- animation: config.animation ?? "fade",
1089
- onDismiss: config.onDismiss,
1090
- sourceTrait: config.sourceTrait
1091
- };
1092
- if (config.autoDismissMs && config.autoDismissMs > 0) {
1093
- content.autoDismissAt = Date.now() + config.autoDismissMs;
1094
- const timer = setTimeout(() => {
1095
- setSlots((prev) => {
1096
- if (prev[config.target]?.id === id) {
1097
- content.onDismiss?.();
1098
- notifySubscribers(config.target, null);
1099
- return { ...prev, [config.target]: null };
1100
- }
1101
- return prev;
1102
- });
1103
- timersRef.current.delete(id);
1104
- }, config.autoDismissMs);
1105
- timersRef.current.set(id, timer);
1106
- }
1107
- setSlots((prev) => {
1108
- const existing = prev[config.target];
1109
- if (existing && existing.priority > content.priority) {
1110
- console.warn(
1111
- `[UISlots] Slot "${config.target}" already has higher priority content (${existing.priority} > ${content.priority})`
1112
- );
1113
- return prev;
1114
- }
1115
- if (content.sourceTrait) {
1116
- indexTraitRender(content.sourceTrait, content);
1117
- notifyTraitSubscribers(content.sourceTrait, content);
1127
+ const render = react.useCallback(
1128
+ (config) => {
1129
+ const id = generateId();
1130
+ const sourceKey = config.sourceTrait ?? DEFAULT_SOURCE_KEY;
1131
+ const content = {
1132
+ id,
1133
+ pattern: config.pattern,
1134
+ props: config.props ?? {},
1135
+ priority: config.priority ?? 0,
1136
+ animation: config.animation ?? "fade",
1137
+ onDismiss: config.onDismiss,
1138
+ sourceTrait: config.sourceTrait
1139
+ };
1140
+ if (config.autoDismissMs && config.autoDismissMs > 0) {
1141
+ content.autoDismissAt = Date.now() + config.autoDismissMs;
1142
+ const timer = setTimeout(() => {
1143
+ setSources((prev) => {
1144
+ const slotSources = prev[config.target];
1145
+ if (slotSources && slotSources[sourceKey]?.id === id) {
1146
+ content.onDismiss?.();
1147
+ const next = { ...slotSources };
1148
+ delete next[sourceKey];
1149
+ const updated = { ...prev, [config.target]: next };
1150
+ notifySubscribers(config.target, aggregateSlot(next));
1151
+ return updated;
1152
+ }
1153
+ return prev;
1154
+ });
1155
+ timersRef.current.delete(id);
1156
+ }, config.autoDismissMs);
1157
+ timersRef.current.set(id, timer);
1118
1158
  }
1119
- notifySubscribers(config.target, content);
1120
- return { ...prev, [config.target]: content };
1121
- });
1122
- return id;
1123
- }, [notifySubscribers, notifyTraitSubscribers, indexTraitRender]);
1124
- const clear = react.useCallback((slot) => {
1125
- setSlots((prev) => {
1126
- const content = prev[slot];
1127
- if (content) {
1128
- const timer = timersRef.current.get(content.id);
1129
- if (timer) {
1130
- clearTimeout(timer);
1131
- timersRef.current.delete(content.id);
1159
+ setSources((prev) => {
1160
+ const slotSources = prev[config.target] ?? {};
1161
+ const existing = slotSources[sourceKey];
1162
+ if (existing && existing.priority > content.priority) {
1163
+ console.warn(
1164
+ `[UISlots] Slot "${config.target}" source "${sourceKey}" already has higher priority content (${existing.priority} > ${content.priority})`
1165
+ );
1166
+ return prev;
1132
1167
  }
1133
- content.onDismiss?.();
1168
+ const nextSources = {
1169
+ ...slotSources,
1170
+ [sourceKey]: content
1171
+ };
1172
+ const nextAll = { ...prev, [config.target]: nextSources };
1134
1173
  if (content.sourceTrait) {
1135
- unindexTrait(content.sourceTrait);
1136
- notifyTraitSubscribers(content.sourceTrait, null);
1174
+ indexTraitRender(content.sourceTrait, content);
1175
+ notifyTraitSubscribers(content.sourceTrait, content);
1176
+ }
1177
+ notifySubscribers(config.target, aggregateSlot(nextSources));
1178
+ return nextAll;
1179
+ });
1180
+ return id;
1181
+ },
1182
+ [notifySubscribers, notifyTraitSubscribers, indexTraitRender]
1183
+ );
1184
+ const clear = react.useCallback(
1185
+ (slot) => {
1186
+ setSources((prev) => {
1187
+ const slotSources = prev[slot];
1188
+ if (!slotSources || Object.keys(slotSources).length === 0) {
1189
+ return prev;
1190
+ }
1191
+ for (const content of Object.values(slotSources)) {
1192
+ const timer = timersRef.current.get(content.id);
1193
+ if (timer) {
1194
+ clearTimeout(timer);
1195
+ timersRef.current.delete(content.id);
1196
+ }
1197
+ content.onDismiss?.();
1198
+ if (content.sourceTrait) {
1199
+ unindexTrait(content.sourceTrait);
1200
+ notifyTraitSubscribers(content.sourceTrait, null);
1201
+ }
1137
1202
  }
1138
1203
  notifySubscribers(slot, null);
1139
- }
1140
- return { ...prev, [slot]: null };
1141
- });
1142
- }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
1143
- const clearById = react.useCallback((id) => {
1144
- setSlots((prev) => {
1145
- const entry = Object.entries(prev).find(([, content]) => content?.id === id);
1146
- if (entry) {
1147
- const [slot, content] = entry;
1148
- const timer = timersRef.current.get(id);
1204
+ return { ...prev, [slot]: {} };
1205
+ });
1206
+ },
1207
+ [notifySubscribers, notifyTraitSubscribers, unindexTrait]
1208
+ );
1209
+ const clearBySource = react.useCallback(
1210
+ (slot, sourceTrait) => {
1211
+ const sourceKey = sourceTrait;
1212
+ setSources((prev) => {
1213
+ const slotSources = prev[slot];
1214
+ if (!slotSources || !(sourceKey in slotSources)) return prev;
1215
+ const content = slotSources[sourceKey];
1216
+ const timer = timersRef.current.get(content.id);
1149
1217
  if (timer) {
1150
1218
  clearTimeout(timer);
1151
- timersRef.current.delete(id);
1219
+ timersRef.current.delete(content.id);
1152
1220
  }
1153
1221
  content.onDismiss?.();
1154
1222
  if (content.sourceTrait) {
1155
1223
  unindexTrait(content.sourceTrait);
1156
1224
  notifyTraitSubscribers(content.sourceTrait, null);
1157
1225
  }
1158
- notifySubscribers(slot, null);
1159
- return { ...prev, [slot]: null };
1160
- }
1161
- return prev;
1162
- });
1163
- }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
1226
+ const nextSources = { ...slotSources };
1227
+ delete nextSources[sourceKey];
1228
+ notifySubscribers(slot, aggregateSlot(nextSources));
1229
+ return { ...prev, [slot]: nextSources };
1230
+ });
1231
+ },
1232
+ [notifySubscribers, notifyTraitSubscribers, unindexTrait]
1233
+ );
1234
+ const clearById = react.useCallback(
1235
+ (id) => {
1236
+ setSources((prev) => {
1237
+ for (const slot of ALL_SLOTS) {
1238
+ const slotSources = prev[slot];
1239
+ if (!slotSources) continue;
1240
+ const matchKey = Object.keys(slotSources).find(
1241
+ (k) => slotSources[k].id === id
1242
+ );
1243
+ if (!matchKey) continue;
1244
+ const content = slotSources[matchKey];
1245
+ const timer = timersRef.current.get(id);
1246
+ if (timer) {
1247
+ clearTimeout(timer);
1248
+ timersRef.current.delete(id);
1249
+ }
1250
+ content.onDismiss?.();
1251
+ if (content.sourceTrait) {
1252
+ unindexTrait(content.sourceTrait);
1253
+ notifyTraitSubscribers(content.sourceTrait, null);
1254
+ }
1255
+ const nextSources = { ...slotSources };
1256
+ delete nextSources[matchKey];
1257
+ notifySubscribers(slot, aggregateSlot(nextSources));
1258
+ return { ...prev, [slot]: nextSources };
1259
+ }
1260
+ return prev;
1261
+ });
1262
+ },
1263
+ [notifySubscribers, notifyTraitSubscribers, unindexTrait]
1264
+ );
1164
1265
  const clearAll = react.useCallback(() => {
1165
1266
  timersRef.current.forEach((timer) => clearTimeout(timer));
1166
1267
  timersRef.current.clear();
1167
- setSlots((prev) => {
1168
- Object.entries(prev).forEach(([slot, content]) => {
1169
- if (content) {
1268
+ setSources((prev) => {
1269
+ for (const slot of ALL_SLOTS) {
1270
+ const slotSources = prev[slot];
1271
+ if (!slotSources) continue;
1272
+ for (const content of Object.values(slotSources)) {
1170
1273
  content.onDismiss?.();
1171
1274
  if (content.sourceTrait) {
1172
1275
  notifyTraitSubscribers(content.sourceTrait, null);
1173
1276
  }
1174
- notifySubscribers(slot, null);
1175
1277
  }
1176
- });
1177
- return DEFAULT_SLOTS;
1278
+ notifySubscribers(slot, null);
1279
+ }
1280
+ return DEFAULT_SOURCES;
1178
1281
  });
1179
1282
  traitIndexRef.current.clear();
1180
1283
  }, [notifySubscribers, notifyTraitSubscribers]);
@@ -1184,16 +1287,16 @@ function useUISlotManager() {
1184
1287
  subscribersRef.current.delete(callback);
1185
1288
  };
1186
1289
  }, []);
1187
- const hasContent = react.useCallback((slot) => {
1188
- return slots[slot] !== null;
1189
- }, [slots]);
1190
- const getContent = react.useCallback((slot) => {
1191
- return slots[slot];
1192
- }, [slots]);
1290
+ const hasContent = react.useCallback(
1291
+ (slot) => slots[slot] !== null,
1292
+ [slots]
1293
+ );
1294
+ const getContent = react.useCallback(
1295
+ (slot) => slots[slot],
1296
+ [slots]
1297
+ );
1193
1298
  const getTraitContent = react.useCallback(
1194
- (traitName) => {
1195
- return traitIndexRef.current.get(traitName) ?? null;
1196
- },
1299
+ (traitName) => traitIndexRef.current.get(traitName) ?? null,
1197
1300
  []
1198
1301
  );
1199
1302
  const subscribeTrait = react.useCallback(
@@ -1219,6 +1322,7 @@ function useUISlotManager() {
1219
1322
  slots,
1220
1323
  render,
1221
1324
  clear,
1325
+ clearBySource,
1222
1326
  clearById,
1223
1327
  clearAll,
1224
1328
  subscribe: subscribe2,