@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.
@@ -3,30 +3,76 @@
3
3
  var react = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
 
6
- var DEFAULT_SLOTS = {
7
- main: null,
8
- sidebar: null,
9
- modal: null,
10
- drawer: null,
11
- overlay: null,
12
- center: null,
13
- toast: null,
14
- "hud-top": null,
15
- "hud-bottom": null,
16
- "hud-left": null,
17
- "hud-right": null,
18
- floating: null
19
- };
6
+ var DEFAULT_SOURCE_KEY = "__default__";
7
+ var MULTI_SOURCE_STACK_TRAIT = "__multi_source_stack__";
8
+ var ALL_SLOTS = [
9
+ "main",
10
+ "sidebar",
11
+ "modal",
12
+ "drawer",
13
+ "overlay",
14
+ "center",
15
+ "toast",
16
+ "hud-top",
17
+ "hud-bottom",
18
+ "hud-left",
19
+ "hud-right",
20
+ "floating"
21
+ ];
22
+ var DEFAULT_SLOTS = ALL_SLOTS.reduce(
23
+ (acc, slot) => {
24
+ acc[slot] = null;
25
+ return acc;
26
+ },
27
+ {}
28
+ );
29
+ var DEFAULT_SOURCES = ALL_SLOTS.reduce(
30
+ (acc, slot) => {
31
+ acc[slot] = {};
32
+ return acc;
33
+ },
34
+ {}
35
+ );
20
36
  var idCounter = 0;
21
37
  function generateId() {
22
38
  return `slot-content-${++idCounter}-${Date.now()}`;
23
39
  }
40
+ function aggregateSlot(sources) {
41
+ if (!sources) return null;
42
+ const entries = Object.entries(sources);
43
+ if (entries.length === 0) return null;
44
+ if (entries.length === 1) return entries[0][1];
45
+ const children = entries.map(([, entry]) => ({
46
+ type: entry.pattern,
47
+ ...entry.props
48
+ }));
49
+ const stackId = `slot-content-stack-${entries.map(([k]) => k).join("-")}`;
50
+ return {
51
+ id: stackId,
52
+ pattern: "stack",
53
+ props: {
54
+ direction: "vertical",
55
+ gap: "lg",
56
+ children
57
+ },
58
+ priority: 0,
59
+ animation: "fade",
60
+ sourceTrait: MULTI_SOURCE_STACK_TRAIT
61
+ };
62
+ }
24
63
  function useUISlotManager() {
25
- const [slots, setSlots] = react.useState(DEFAULT_SLOTS);
64
+ const [sources, setSources] = react.useState(DEFAULT_SOURCES);
26
65
  const subscribersRef = react.useRef(/* @__PURE__ */ new Set());
27
66
  const timersRef = react.useRef(/* @__PURE__ */ new Map());
28
67
  const traitIndexRef = react.useRef(/* @__PURE__ */ new Map());
29
68
  const traitSubscribersRef = react.useRef(/* @__PURE__ */ new Map());
69
+ const slots = react.useMemo(() => {
70
+ const out = { ...DEFAULT_SLOTS };
71
+ for (const slot of ALL_SLOTS) {
72
+ out[slot] = aggregateSlot(sources[slot]);
73
+ }
74
+ return out;
75
+ }, [sources]);
30
76
  react.useEffect(() => {
31
77
  return () => {
32
78
  timersRef.current.forEach((timer) => clearTimeout(timer));
@@ -65,103 +111,160 @@ function useUISlotManager() {
65
111
  const unindexTrait = react.useCallback((traitName) => {
66
112
  traitIndexRef.current.delete(traitName);
67
113
  }, []);
68
- const render = react.useCallback((config) => {
69
- const id = generateId();
70
- const content = {
71
- id,
72
- pattern: config.pattern,
73
- props: config.props ?? {},
74
- priority: config.priority ?? 0,
75
- animation: config.animation ?? "fade",
76
- onDismiss: config.onDismiss,
77
- sourceTrait: config.sourceTrait
78
- };
79
- if (config.autoDismissMs && config.autoDismissMs > 0) {
80
- content.autoDismissAt = Date.now() + config.autoDismissMs;
81
- const timer = setTimeout(() => {
82
- setSlots((prev) => {
83
- if (prev[config.target]?.id === id) {
84
- content.onDismiss?.();
85
- notifySubscribers(config.target, null);
86
- return { ...prev, [config.target]: null };
87
- }
88
- return prev;
89
- });
90
- timersRef.current.delete(id);
91
- }, config.autoDismissMs);
92
- timersRef.current.set(id, timer);
93
- }
94
- setSlots((prev) => {
95
- const existing = prev[config.target];
96
- if (existing && existing.priority > content.priority) {
97
- console.warn(
98
- `[UISlots] Slot "${config.target}" already has higher priority content (${existing.priority} > ${content.priority})`
99
- );
100
- return prev;
101
- }
102
- if (content.sourceTrait) {
103
- indexTraitRender(content.sourceTrait, content);
104
- notifyTraitSubscribers(content.sourceTrait, content);
114
+ const render = react.useCallback(
115
+ (config) => {
116
+ const id = generateId();
117
+ const sourceKey = config.sourceTrait ?? DEFAULT_SOURCE_KEY;
118
+ const content = {
119
+ id,
120
+ pattern: config.pattern,
121
+ props: config.props ?? {},
122
+ priority: config.priority ?? 0,
123
+ animation: config.animation ?? "fade",
124
+ onDismiss: config.onDismiss,
125
+ sourceTrait: config.sourceTrait
126
+ };
127
+ if (config.autoDismissMs && config.autoDismissMs > 0) {
128
+ content.autoDismissAt = Date.now() + config.autoDismissMs;
129
+ const timer = setTimeout(() => {
130
+ setSources((prev) => {
131
+ const slotSources = prev[config.target];
132
+ if (slotSources && slotSources[sourceKey]?.id === id) {
133
+ content.onDismiss?.();
134
+ const next = { ...slotSources };
135
+ delete next[sourceKey];
136
+ const updated = { ...prev, [config.target]: next };
137
+ notifySubscribers(config.target, aggregateSlot(next));
138
+ return updated;
139
+ }
140
+ return prev;
141
+ });
142
+ timersRef.current.delete(id);
143
+ }, config.autoDismissMs);
144
+ timersRef.current.set(id, timer);
105
145
  }
106
- notifySubscribers(config.target, content);
107
- return { ...prev, [config.target]: content };
108
- });
109
- return id;
110
- }, [notifySubscribers, notifyTraitSubscribers, indexTraitRender]);
111
- const clear = react.useCallback((slot) => {
112
- setSlots((prev) => {
113
- const content = prev[slot];
114
- if (content) {
115
- const timer = timersRef.current.get(content.id);
116
- if (timer) {
117
- clearTimeout(timer);
118
- timersRef.current.delete(content.id);
146
+ setSources((prev) => {
147
+ const slotSources = prev[config.target] ?? {};
148
+ const existing = slotSources[sourceKey];
149
+ if (existing && existing.priority > content.priority) {
150
+ console.warn(
151
+ `[UISlots] Slot "${config.target}" source "${sourceKey}" already has higher priority content (${existing.priority} > ${content.priority})`
152
+ );
153
+ return prev;
119
154
  }
120
- content.onDismiss?.();
155
+ const nextSources = {
156
+ ...slotSources,
157
+ [sourceKey]: content
158
+ };
159
+ const nextAll = { ...prev, [config.target]: nextSources };
121
160
  if (content.sourceTrait) {
122
- unindexTrait(content.sourceTrait);
123
- notifyTraitSubscribers(content.sourceTrait, null);
161
+ indexTraitRender(content.sourceTrait, content);
162
+ notifyTraitSubscribers(content.sourceTrait, content);
163
+ }
164
+ notifySubscribers(config.target, aggregateSlot(nextSources));
165
+ return nextAll;
166
+ });
167
+ return id;
168
+ },
169
+ [notifySubscribers, notifyTraitSubscribers, indexTraitRender]
170
+ );
171
+ const clear = react.useCallback(
172
+ (slot) => {
173
+ setSources((prev) => {
174
+ const slotSources = prev[slot];
175
+ if (!slotSources || Object.keys(slotSources).length === 0) {
176
+ return prev;
177
+ }
178
+ for (const content of Object.values(slotSources)) {
179
+ const timer = timersRef.current.get(content.id);
180
+ if (timer) {
181
+ clearTimeout(timer);
182
+ timersRef.current.delete(content.id);
183
+ }
184
+ content.onDismiss?.();
185
+ if (content.sourceTrait) {
186
+ unindexTrait(content.sourceTrait);
187
+ notifyTraitSubscribers(content.sourceTrait, null);
188
+ }
124
189
  }
125
190
  notifySubscribers(slot, null);
126
- }
127
- return { ...prev, [slot]: null };
128
- });
129
- }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
130
- const clearById = react.useCallback((id) => {
131
- setSlots((prev) => {
132
- const entry = Object.entries(prev).find(([, content]) => content?.id === id);
133
- if (entry) {
134
- const [slot, content] = entry;
135
- const timer = timersRef.current.get(id);
191
+ return { ...prev, [slot]: {} };
192
+ });
193
+ },
194
+ [notifySubscribers, notifyTraitSubscribers, unindexTrait]
195
+ );
196
+ const clearBySource = react.useCallback(
197
+ (slot, sourceTrait) => {
198
+ const sourceKey = sourceTrait;
199
+ setSources((prev) => {
200
+ const slotSources = prev[slot];
201
+ if (!slotSources || !(sourceKey in slotSources)) return prev;
202
+ const content = slotSources[sourceKey];
203
+ const timer = timersRef.current.get(content.id);
136
204
  if (timer) {
137
205
  clearTimeout(timer);
138
- timersRef.current.delete(id);
206
+ timersRef.current.delete(content.id);
139
207
  }
140
208
  content.onDismiss?.();
141
209
  if (content.sourceTrait) {
142
210
  unindexTrait(content.sourceTrait);
143
211
  notifyTraitSubscribers(content.sourceTrait, null);
144
212
  }
145
- notifySubscribers(slot, null);
146
- return { ...prev, [slot]: null };
147
- }
148
- return prev;
149
- });
150
- }, [notifySubscribers, notifyTraitSubscribers, unindexTrait]);
213
+ const nextSources = { ...slotSources };
214
+ delete nextSources[sourceKey];
215
+ notifySubscribers(slot, aggregateSlot(nextSources));
216
+ return { ...prev, [slot]: nextSources };
217
+ });
218
+ },
219
+ [notifySubscribers, notifyTraitSubscribers, unindexTrait]
220
+ );
221
+ const clearById = react.useCallback(
222
+ (id) => {
223
+ setSources((prev) => {
224
+ for (const slot of ALL_SLOTS) {
225
+ const slotSources = prev[slot];
226
+ if (!slotSources) continue;
227
+ const matchKey = Object.keys(slotSources).find(
228
+ (k) => slotSources[k].id === id
229
+ );
230
+ if (!matchKey) continue;
231
+ const content = slotSources[matchKey];
232
+ const timer = timersRef.current.get(id);
233
+ if (timer) {
234
+ clearTimeout(timer);
235
+ timersRef.current.delete(id);
236
+ }
237
+ content.onDismiss?.();
238
+ if (content.sourceTrait) {
239
+ unindexTrait(content.sourceTrait);
240
+ notifyTraitSubscribers(content.sourceTrait, null);
241
+ }
242
+ const nextSources = { ...slotSources };
243
+ delete nextSources[matchKey];
244
+ notifySubscribers(slot, aggregateSlot(nextSources));
245
+ return { ...prev, [slot]: nextSources };
246
+ }
247
+ return prev;
248
+ });
249
+ },
250
+ [notifySubscribers, notifyTraitSubscribers, unindexTrait]
251
+ );
151
252
  const clearAll = react.useCallback(() => {
152
253
  timersRef.current.forEach((timer) => clearTimeout(timer));
153
254
  timersRef.current.clear();
154
- setSlots((prev) => {
155
- Object.entries(prev).forEach(([slot, content]) => {
156
- if (content) {
255
+ setSources((prev) => {
256
+ for (const slot of ALL_SLOTS) {
257
+ const slotSources = prev[slot];
258
+ if (!slotSources) continue;
259
+ for (const content of Object.values(slotSources)) {
157
260
  content.onDismiss?.();
158
261
  if (content.sourceTrait) {
159
262
  notifyTraitSubscribers(content.sourceTrait, null);
160
263
  }
161
- notifySubscribers(slot, null);
162
264
  }
163
- });
164
- return DEFAULT_SLOTS;
265
+ notifySubscribers(slot, null);
266
+ }
267
+ return DEFAULT_SOURCES;
165
268
  });
166
269
  traitIndexRef.current.clear();
167
270
  }, [notifySubscribers, notifyTraitSubscribers]);
@@ -171,16 +274,16 @@ function useUISlotManager() {
171
274
  subscribersRef.current.delete(callback);
172
275
  };
173
276
  }, []);
174
- const hasContent = react.useCallback((slot) => {
175
- return slots[slot] !== null;
176
- }, [slots]);
177
- const getContent = react.useCallback((slot) => {
178
- return slots[slot];
179
- }, [slots]);
277
+ const hasContent = react.useCallback(
278
+ (slot) => slots[slot] !== null,
279
+ [slots]
280
+ );
281
+ const getContent = react.useCallback(
282
+ (slot) => slots[slot],
283
+ [slots]
284
+ );
180
285
  const getTraitContent = react.useCallback(
181
- (traitName) => {
182
- return traitIndexRef.current.get(traitName) ?? null;
183
- },
286
+ (traitName) => traitIndexRef.current.get(traitName) ?? null,
184
287
  []
185
288
  );
186
289
  const subscribeTrait = react.useCallback(
@@ -206,6 +309,7 @@ function useUISlotManager() {
206
309
  slots,
207
310
  render,
208
311
  clear,
312
+ clearBySource,
209
313
  clearById,
210
314
  clearAll,
211
315
  subscribe,