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