universe-code 0.0.85 → 0.0.87

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 +1,2 @@
1
- export { IdbService } from './indexdb/services/idbService.js';
1
+ export { IdbService } from './indexdb/services/idbService.js';
2
+ export { ColorSparkService, initSpark, singleSpark } from './spark/index.js';
@@ -0,0 +1,328 @@
1
+ class ColorSparkService {
2
+ constructor() {
3
+ this.observers = new Map();
4
+ this.intervals = new Map();
5
+ this.previousValues = new Map();
6
+
7
+ this._injectStyles(); // <-- inject CSS on service creation
8
+ }
9
+
10
+ _injectStyles() {
11
+ if (document.getElementById('color-spark-style')) return;
12
+
13
+ const style = document.createElement('style');
14
+ style.id = 'color-spark-style';
15
+
16
+ style.textContent = `
17
+ @-webkit-keyframes anim-opac {
18
+ from { opacity: 1; }
19
+ to { opacity: 0; }
20
+ }
21
+
22
+ @keyframes anim-opac {
23
+ from { opacity: 1; }
24
+ to { opacity: 0; }
25
+ }
26
+
27
+ /* BACK highlight */
28
+ .spark-back::before {
29
+ -webkit-animation: anim-opac 1s both;
30
+ animation: anim-opac 1s both;
31
+ position: absolute;
32
+ inset: 0;
33
+ background: var(--spark-back-bg, #f8e71c);
34
+ border: 1px solid var(--spark-back-border, #f8e71c);
35
+ content: "";
36
+ border-radius: inherit;
37
+ width: 100%;
38
+ height: 100%;
39
+ pointer-events: none;
40
+ }
41
+
42
+ /* LAY highlight */
43
+ .spark-lay::before {
44
+ -webkit-animation: anim-opac 1s both;
45
+ animation: anim-opac 1s both;
46
+ position: absolute;
47
+ inset: 0;
48
+ background: var(--spark-lay-bg, #ff3b30);
49
+ border: 1px solid var(--spark-lay-border, #ff3b30);
50
+ content: "";
51
+ border-radius: inherit;
52
+ width: 100%;
53
+ height: 100%;
54
+ pointer-events: none;
55
+ }
56
+ `;
57
+
58
+ document.head.appendChild(style);
59
+ }
60
+
61
+
62
+
63
+ /**
64
+ * Monitor a single element for value changes and apply spark effect
65
+ *
66
+ * @param {HTMLElement} element - Element to monitor
67
+ * @returns {Function} Cleanup function to stop monitoring
68
+ */
69
+ sparkElement(element) {
70
+ if (!element || !(element instanceof HTMLElement)) {
71
+ console.warn('[ColorSpark] Invalid element provided');
72
+ return () => { };
73
+ }
74
+
75
+ const elementId = this._getElementId(element);
76
+
77
+ // 1️⃣ Watch element visibility (performance optimization)
78
+ const visibilityObserver = new IntersectionObserver((entries) => {
79
+ entries.forEach((entry) => {
80
+ if (entry.isIntersecting) {
81
+ this._startMonitoring(element, elementId);
82
+ } else {
83
+ this._stopMonitoring(elementId);
84
+ }
85
+ });
86
+ });
87
+
88
+ visibilityObserver.observe(element);
89
+ this.observers.set(elementId, visibilityObserver);
90
+
91
+ // 2️⃣ Watch for element removal from DOM
92
+ const removalObserver = new MutationObserver((mutations) => {
93
+ mutations.forEach((mutation) => {
94
+ mutation.removedNodes.forEach((node) => {
95
+ if (node === element) {
96
+ this.cleanup(elementId);
97
+ removalObserver.disconnect();
98
+ }
99
+ });
100
+ });
101
+ });
102
+
103
+ removalObserver.observe(document.body, {
104
+ childList: true,
105
+ subtree: true,
106
+ });
107
+
108
+ // Return cleanup function
109
+ return () => {
110
+ this.cleanup(elementId);
111
+ removalObserver.disconnect();
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Initialize global spark monitoring for all [data-color-spark] elements
117
+ *
118
+ * @returns {Function} Global cleanup function
119
+ */
120
+ initGlobal() {
121
+ if (typeof document === 'undefined') {
122
+ console.warn('[ColorSpark] Document not available (SSR?)');
123
+ return () => { };
124
+ }
125
+
126
+ const cleanupFunctions = [];
127
+
128
+ // 3️⃣ Find and monitor all existing elements
129
+ const existingElements = document.querySelectorAll('[data-color-spark]');
130
+
131
+ existingElements.forEach((el) => {
132
+ if (el instanceof HTMLElement) {
133
+ const cleanup = this.sparkElement(el);
134
+ cleanupFunctions.push(cleanup);
135
+ }
136
+ });
137
+
138
+ // 4️⃣ Watch for new elements added to DOM
139
+ const dynamicObserver = new MutationObserver((mutations) => {
140
+ mutations.forEach((mutation) => {
141
+ mutation.addedNodes.forEach((node) => {
142
+ if (!(node instanceof HTMLElement)) return;
143
+
144
+ // Check if node itself has the attribute
145
+ if (node.hasAttribute('data-color-spark')) {
146
+ const cleanup = this.sparkElement(node);
147
+ cleanupFunctions.push(cleanup);
148
+ }
149
+
150
+ // Check children for the attribute
151
+ const children = node.querySelectorAll('[data-color-spark]');
152
+ children.forEach((child) => {
153
+ if (child instanceof HTMLElement) {
154
+ const cleanup = this.sparkElement(child);
155
+ cleanupFunctions.push(cleanup);
156
+ }
157
+ });
158
+ });
159
+ });
160
+ });
161
+
162
+ dynamicObserver.observe(document.body, {
163
+ childList: true,
164
+ subtree: true,
165
+ });
166
+
167
+ // 5️⃣ Return global cleanup
168
+ return () => {
169
+ dynamicObserver.disconnect();
170
+ cleanupFunctions.forEach((fn) => fn?.());
171
+ this.cleanupAll();
172
+ };
173
+ }
174
+
175
+ /* ================================
176
+ * INTERNAL LOGIC
177
+ * ================================ */
178
+
179
+ /**
180
+ * Start monitoring element for text changes
181
+ * @private
182
+ */
183
+ _startMonitoring(element, elementId) {
184
+ // Already monitoring
185
+ if (this.intervals.has(elementId)) return;
186
+
187
+ const checkInterval = setInterval(() => {
188
+ const currentValue = element.innerText || element.textContent || '';
189
+ const previousValue = this.previousValues.get(elementId);
190
+
191
+ // Value changed → Apply spark effect
192
+ if (
193
+ previousValue !== null &&
194
+ previousValue !== undefined &&
195
+ previousValue !== currentValue
196
+ ) {
197
+ const sparkClass = this._getSparkClass(element);
198
+
199
+ if (sparkClass) {
200
+ element.classList.add(sparkClass);
201
+
202
+ // Remove after animation completes
203
+ setTimeout(() => {
204
+ element.classList.remove(sparkClass);
205
+ }, 600);
206
+ }
207
+ }
208
+
209
+ this.previousValues.set(elementId, currentValue);
210
+ }, 600);
211
+
212
+ this.intervals.set(elementId, checkInterval);
213
+ }
214
+
215
+ /**
216
+ * Stop monitoring element
217
+ * @private
218
+ */
219
+ _stopMonitoring(elementId) {
220
+ const interval = this.intervals.get(elementId);
221
+
222
+ if (interval) {
223
+ clearInterval(interval);
224
+ this.intervals.delete(elementId);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Determine which spark animation to apply based on element classes
230
+ * @private
231
+ */
232
+ _getSparkClass(element) {
233
+ // Check element itself
234
+ if (this._isBackElement(element)) return 'spark-back';
235
+ if (this._isLayElement(element)) return 'spark-lay';
236
+
237
+ // Check parent element
238
+ const parent = element.closest('.back, .lay, .btn-back, .btn-lay');
239
+
240
+ if (parent) {
241
+ if (this._isBackElement(parent)) return 'spark-back';
242
+ if (this._isLayElement(parent)) return 'spark-lay';
243
+ }
244
+
245
+ // Default to back animation
246
+ return 'spark-back';
247
+ }
248
+
249
+ /**
250
+ * Check if element has BACK-related classes
251
+ * @private
252
+ */
253
+ _isBackElement(element) {
254
+ return (
255
+ element.classList.contains('back') ||
256
+ element.classList.contains('btn-back')
257
+ );
258
+ }
259
+
260
+ /**
261
+ * Check if element has LAY-related classes
262
+ * @private
263
+ */
264
+ _isLayElement(element) {
265
+ return (
266
+ element.classList.contains('lay') ||
267
+ element.classList.contains('btn-lay')
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Cleanup specific element's monitoring
273
+ *
274
+ * @param {string} elementId - Unique element identifier
275
+ */
276
+ cleanup(elementId) {
277
+ this._stopMonitoring(elementId);
278
+
279
+ const observer = this.observers.get(elementId);
280
+ if (observer) {
281
+ observer.disconnect();
282
+ this.observers.delete(elementId);
283
+ }
284
+
285
+ this.previousValues.delete(elementId);
286
+ }
287
+
288
+ /**
289
+ * Cleanup all monitoring (called on destroy)
290
+ */
291
+ cleanupAll() {
292
+ this.intervals.forEach((interval) => clearInterval(interval));
293
+ this.observers.forEach((observer) => observer.disconnect());
294
+
295
+ this.intervals.clear();
296
+ this.observers.clear();
297
+ this.previousValues.clear();
298
+ }
299
+
300
+ /**
301
+ * Generate or retrieve unique ID for element
302
+ * @private
303
+ */
304
+ _getElementId(element) {
305
+ const idKey = '__colorSparkId';
306
+
307
+ if (!element[idKey]) {
308
+ element[idKey] = 'spark_' + Math.random().toString(36).slice(2, 10);
309
+ }
310
+
311
+ return element[idKey];
312
+ }
313
+ }
314
+
315
+
316
+ function initSpark() {
317
+ const service = new ColorSparkService();
318
+ return service.initGlobal();
319
+ }
320
+
321
+
322
+ function singleSpark(element) {
323
+ const service = new ColorSparkService();
324
+ return service.sparkElement(element);
325
+ }
326
+
327
+
328
+ export { ColorSparkService, initSpark, singleSpark };
@@ -1,13 +1,15 @@
1
1
  import { getIdbStore } from './indexdb/store/idbStore.js';
2
2
  import { useIdbStore } from './indexdb/hooks/idbHook.js';
3
3
  import { configureIdb } from "./indexdb/store/config.js";
4
+ import { useColorSpark } from "./spark/index.js";
4
5
 
5
6
  // Export everything as a unified object or individual pieces
6
- export { configureIdb, getIdbStore, useIdbStore };
7
+ export { configureIdb, getIdbStore, useIdbStore, useColorSpark };
7
8
 
8
9
  // Default export for easy usage
9
10
  export default {
10
11
  configureIdb,
11
12
  getIdbStore,
12
13
  useIdbStore,
14
+ useColorSpark
13
15
  };
@@ -0,0 +1,144 @@
1
+ import { useEffect } from "react";
2
+
3
+ export function useColorSpark() {
4
+ useEffect(() => {
5
+ injectSparkStyles();
6
+
7
+ function injectSparkStyles() {
8
+ if (document.getElementById("spark-style")) return;
9
+
10
+ const style = document.createElement("style");
11
+ style.id = "spark-style";
12
+
13
+ style.textContent = `
14
+ .spark-back::before {
15
+ -webkit-animation: anim-opac 1s both;
16
+ animation: anim-opac 1s both;
17
+ position: absolute;
18
+ top: 0;
19
+ bottom: 0;
20
+ left: 0;
21
+ right: 0;
22
+ background: var(--spark-back-bg, #f8e71c);
23
+ border: 1px solid var(--spark-back-border, #f8e71c);
24
+ content: " ";
25
+ border-radius: inherit;
26
+ display: flex;
27
+ width: 100%;
28
+ height: 100%;
29
+ }
30
+
31
+ @keyframes anim-opac {
32
+ from {
33
+ opacity: 1;
34
+ }
35
+ to {
36
+ opacity: 0;
37
+ }
38
+ }
39
+
40
+ .spark-lay::before {
41
+ -webkit-animation: anim-opac 1s both;
42
+ animation: anim-opac 1s both;
43
+ position: absolute;
44
+ top: 0;
45
+ bottom: 0;
46
+ left: 0;
47
+ right: 0;
48
+ background: var(--spark-lay-bg, #ff3b30);
49
+ border: 1px solid var(--spark-lay-border, #ff3b30);
50
+ content: " ";
51
+ border-radius: inherit;
52
+ display: flex;
53
+ width: 100%;
54
+ height: 100%;
55
+ }`;
56
+ document.head.appendChild(style);
57
+ }
58
+
59
+ function attachColorSpark(el) {
60
+ let previousValue = null;
61
+ let intervalId = null;
62
+ let isVisible = false;
63
+
64
+ const visibilityObserver = new IntersectionObserver((entries) => {
65
+ entries.forEach((entry) => {
66
+ isVisible = entry.isIntersecting;
67
+ if (isVisible) start();
68
+ else stop();
69
+ });
70
+ });
71
+
72
+ visibilityObserver.observe(el);
73
+
74
+ function start() {
75
+ if (intervalId) return;
76
+
77
+ intervalId = window.setInterval(() => {
78
+ const currentValue = el.innerText.trim();
79
+
80
+ if (previousValue !== null && previousValue !== currentValue) {
81
+ if (el.classList.contains("back")) {
82
+ el.classList.add("spark-back");
83
+ } else if (el.classList.contains("lay")) {
84
+ el.classList.add("spark-lay");
85
+ }
86
+ } else {
87
+ el.classList.remove("spark-back");
88
+ el.classList.remove("spark-lay");
89
+ }
90
+
91
+ previousValue = currentValue;
92
+ }, 600);
93
+ }
94
+
95
+ function stop() {
96
+ if (!intervalId) return;
97
+ clearInterval(intervalId);
98
+ intervalId = null;
99
+ }
100
+
101
+ const domObserver = new MutationObserver((mutations) => {
102
+ mutations.forEach((mutation) => {
103
+ mutation.removedNodes.forEach((node) => {
104
+ if (node === el) {
105
+ stop();
106
+ visibilityObserver.disconnect();
107
+ domObserver.disconnect();
108
+ }
109
+ });
110
+ });
111
+ });
112
+
113
+ domObserver.observe(document.body, { childList: true, subtree: true });
114
+ }
115
+
116
+ // existing elements
117
+ Array.from(
118
+ document.querySelectorAll("[data-color-spark]")
119
+ ).forEach(attachColorSpark);
120
+
121
+ // future elements
122
+ const rootObserver = new MutationObserver((mutations) => {
123
+ mutations.forEach((mutation) => {
124
+ mutation.addedNodes.forEach((node) => {
125
+ if (!(node instanceof HTMLElement)) return;
126
+
127
+ if (node.hasAttribute("data-color-spark")) {
128
+ attachColorSpark(node);
129
+ }
130
+
131
+ Array.from(
132
+ node.querySelectorAll("[data-color-spark]")
133
+ ).forEach(attachColorSpark);
134
+ });
135
+ });
136
+ });
137
+
138
+ rootObserver.observe(document.body, { childList: true, subtree: true });
139
+
140
+ return () => {
141
+ rootObserver.disconnect();
142
+ };
143
+ }, []);
144
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universe-code",
3
- "version": "0.0.85",
3
+ "version": "0.0.87",
4
4
  "description": "Universal utility functions for all JS frameworks",
5
5
  "license": "ISC",
6
6
  "type": "module",