pulse-js-framework 1.0.0 → 1.4.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.
package/index.js CHANGED
@@ -11,7 +11,7 @@ export * from './runtime/index.js';
11
11
  export { compile, parse, tokenize } from './compiler/index.js';
12
12
 
13
13
  // Version
14
- export const VERSION = '1.0.0';
14
+ export const VERSION = '1.4.0';
15
15
 
16
16
  // Default export
17
17
  export default {
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Pulse Native Bridge
3
+ * Provides unified API for native mobile features
4
+ * Zero dependencies - works on Android, iOS, and Web
5
+ */
6
+
7
+ (function() {
8
+ 'use strict';
9
+
10
+ // Detect platform
11
+ const isAndroid = typeof window.PulseNative !== 'undefined';
12
+ const isIOS = typeof window.webkit?.messageHandlers?.PulseNative !== 'undefined';
13
+ const isNative = isAndroid || isIOS;
14
+
15
+ // Callback registry for async operations (iOS)
16
+ const callbacks = new Map();
17
+ let callbackId = 0;
18
+
19
+ /**
20
+ * Generate unique callback ID
21
+ */
22
+ function generateCallbackId() {
23
+ return `cb_${++callbackId}_${Date.now()}`;
24
+ }
25
+
26
+ /**
27
+ * Global callback handler (called from native iOS)
28
+ */
29
+ window.__pulseNativeCallback = function(id, response) {
30
+ const callback = callbacks.get(id);
31
+ if (callback) {
32
+ callbacks.delete(id);
33
+ if (response.success) {
34
+ callback.resolve(response.data);
35
+ } else {
36
+ callback.reject(new Error(response.error));
37
+ }
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Call native method (iOS)
43
+ */
44
+ function callNativeIOS(action, args) {
45
+ return new Promise((resolve, reject) => {
46
+ const id = generateCallbackId();
47
+ callbacks.set(id, { resolve, reject });
48
+
49
+ window.webkit.messageHandlers.PulseNative.postMessage({
50
+ action,
51
+ args,
52
+ callbackId: id
53
+ });
54
+
55
+ // Timeout after 30 seconds
56
+ setTimeout(() => {
57
+ if (callbacks.has(id)) {
58
+ callbacks.delete(id);
59
+ reject(new Error('Native call timeout'));
60
+ }
61
+ }, 30000);
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Call native method (Android - synchronous)
67
+ */
68
+ function callNativeAndroid(action, args) {
69
+ return new Promise((resolve, reject) => {
70
+ try {
71
+ let result;
72
+
73
+ switch (action) {
74
+ // Storage
75
+ case 'setItem':
76
+ window.PulseNative.setItem(args.key, args.value);
77
+ resolve();
78
+ break;
79
+ case 'getItem':
80
+ result = window.PulseNative.getItem(args.key);
81
+ resolve(result);
82
+ break;
83
+ case 'removeItem':
84
+ window.PulseNative.removeItem(args.key);
85
+ resolve();
86
+ break;
87
+ case 'clearStorage':
88
+ window.PulseNative.clearStorage();
89
+ resolve();
90
+ break;
91
+ case 'getAllKeys':
92
+ result = window.PulseNative.getAllKeys();
93
+ resolve(result ? result.split(',').filter(k => k) : []);
94
+ break;
95
+
96
+ // Device
97
+ case 'getDeviceInfo':
98
+ result = window.PulseNative.getDeviceInfo();
99
+ resolve(JSON.parse(result));
100
+ break;
101
+ case 'getNetworkStatus':
102
+ result = window.PulseNative.getNetworkStatus();
103
+ resolve(JSON.parse(result));
104
+ break;
105
+
106
+ // UI
107
+ case 'showToast':
108
+ window.PulseNative.showToast(args.message, args.isLong || false);
109
+ resolve();
110
+ break;
111
+ case 'vibrate':
112
+ window.PulseNative.vibrate(args.duration || 100);
113
+ resolve();
114
+ break;
115
+
116
+ // Clipboard
117
+ case 'copyToClipboard':
118
+ window.PulseNative.copyToClipboard(args.text);
119
+ resolve();
120
+ break;
121
+ case 'getClipboardText':
122
+ result = window.PulseNative.getClipboardText();
123
+ resolve(result);
124
+ break;
125
+
126
+ // App Lifecycle
127
+ case 'exitApp':
128
+ window.PulseNative.exitApp();
129
+ resolve();
130
+ break;
131
+ case 'minimizeApp':
132
+ window.PulseNative.minimizeApp();
133
+ resolve();
134
+ break;
135
+
136
+ default:
137
+ reject(new Error(`Unknown action: ${action}`));
138
+ }
139
+ } catch (error) {
140
+ reject(error);
141
+ }
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Call native method
147
+ */
148
+ function callNative(action, args = {}) {
149
+ if (!isNative) {
150
+ return Promise.reject(new Error('Not running in native environment'));
151
+ }
152
+ return isIOS ? callNativeIOS(action, args) : callNativeAndroid(action, args);
153
+ }
154
+
155
+ // =========================================================================
156
+ // Public API
157
+ // =========================================================================
158
+
159
+ const PulseMobile = {
160
+ /**
161
+ * Platform detection
162
+ */
163
+ isNative,
164
+ isAndroid,
165
+ isIOS,
166
+
167
+ get platform() {
168
+ if (isAndroid) return 'android';
169
+ if (isIOS) return 'ios';
170
+ return 'web';
171
+ },
172
+
173
+ // =========================================================================
174
+ // Storage API - Native key-value storage
175
+ // =========================================================================
176
+ Storage: {
177
+ async setItem(key, value) {
178
+ if (!isNative) {
179
+ localStorage.setItem(key, value);
180
+ return;
181
+ }
182
+ return callNative('setItem', { key, value: String(value) });
183
+ },
184
+
185
+ async getItem(key) {
186
+ if (!isNative) {
187
+ return localStorage.getItem(key);
188
+ }
189
+ return callNative('getItem', { key });
190
+ },
191
+
192
+ async removeItem(key) {
193
+ if (!isNative) {
194
+ localStorage.removeItem(key);
195
+ return;
196
+ }
197
+ return callNative('removeItem', { key });
198
+ },
199
+
200
+ async clear() {
201
+ if (!isNative) {
202
+ localStorage.clear();
203
+ return;
204
+ }
205
+ return callNative('clearStorage');
206
+ },
207
+
208
+ async keys() {
209
+ if (!isNative) {
210
+ return Object.keys(localStorage);
211
+ }
212
+ return callNative('getAllKeys');
213
+ },
214
+
215
+ async getObject(key) {
216
+ const value = await this.getItem(key);
217
+ if (!value) return null;
218
+ try {
219
+ return JSON.parse(value);
220
+ } catch {
221
+ return null;
222
+ }
223
+ },
224
+
225
+ async setObject(key, value) {
226
+ return this.setItem(key, JSON.stringify(value));
227
+ }
228
+ },
229
+
230
+ // =========================================================================
231
+ // Device API - Device information
232
+ // =========================================================================
233
+ Device: {
234
+ async getInfo() {
235
+ if (!isNative) {
236
+ return {
237
+ platform: 'web',
238
+ userAgent: navigator.userAgent,
239
+ language: navigator.language,
240
+ online: navigator.onLine
241
+ };
242
+ }
243
+ return callNative('getDeviceInfo');
244
+ },
245
+
246
+ async getNetworkStatus() {
247
+ if (!isNative) {
248
+ return {
249
+ connected: navigator.onLine,
250
+ type: 'unknown'
251
+ };
252
+ }
253
+ return callNative('getNetworkStatus');
254
+ },
255
+
256
+ onNetworkChange(callback) {
257
+ window.addEventListener('online', () => callback({ connected: true }));
258
+ window.addEventListener('offline', () => callback({ connected: false }));
259
+ window.addEventListener('pulse:networkChange', (e) => callback(e.detail));
260
+ }
261
+ },
262
+
263
+ // =========================================================================
264
+ // UI API - Native UI interactions
265
+ // =========================================================================
266
+ UI: {
267
+ async showToast(message, isLong = false) {
268
+ if (!isNative) {
269
+ // Web fallback
270
+ const toast = document.createElement('div');
271
+ toast.textContent = message;
272
+ toast.style.cssText = `
273
+ position: fixed;
274
+ bottom: 20px;
275
+ left: 50%;
276
+ transform: translateX(-50%);
277
+ background: rgba(0,0,0,0.8);
278
+ color: white;
279
+ padding: 12px 24px;
280
+ border-radius: 8px;
281
+ z-index: 99999;
282
+ font-size: 14px;
283
+ font-family: system-ui, sans-serif;
284
+ animation: pulse-toast-in 0.3s ease;
285
+ `;
286
+ document.body.appendChild(toast);
287
+ setTimeout(() => {
288
+ toast.style.animation = 'pulse-toast-out 0.3s ease forwards';
289
+ setTimeout(() => toast.remove(), 300);
290
+ }, isLong ? 3500 : 2000);
291
+ return;
292
+ }
293
+ return callNative('showToast', { message, isLong });
294
+ },
295
+
296
+ async vibrate(duration = 100) {
297
+ if (!isNative) {
298
+ if (navigator.vibrate) {
299
+ navigator.vibrate(duration);
300
+ }
301
+ return;
302
+ }
303
+ return callNative('vibrate', { duration });
304
+ }
305
+ },
306
+
307
+ // =========================================================================
308
+ // Clipboard API
309
+ // =========================================================================
310
+ Clipboard: {
311
+ async copy(text) {
312
+ if (!isNative) {
313
+ if (navigator.clipboard) {
314
+ return navigator.clipboard.writeText(text);
315
+ }
316
+ // Fallback
317
+ const textarea = document.createElement('textarea');
318
+ textarea.value = text;
319
+ textarea.style.position = 'fixed';
320
+ textarea.style.opacity = '0';
321
+ document.body.appendChild(textarea);
322
+ textarea.select();
323
+ document.execCommand('copy');
324
+ document.body.removeChild(textarea);
325
+ return;
326
+ }
327
+ return callNative('copyToClipboard', { text });
328
+ },
329
+
330
+ async read() {
331
+ if (!isNative) {
332
+ if (navigator.clipboard) {
333
+ return navigator.clipboard.readText();
334
+ }
335
+ return '';
336
+ }
337
+ return callNative('getClipboardText');
338
+ }
339
+ },
340
+
341
+ // =========================================================================
342
+ // App Lifecycle
343
+ // =========================================================================
344
+ App: {
345
+ async exit() {
346
+ if (!isNative || isIOS) {
347
+ console.warn('Cannot exit app on this platform');
348
+ return;
349
+ }
350
+ return callNative('exitApp');
351
+ },
352
+
353
+ async minimize() {
354
+ if (!isNative) {
355
+ console.warn('Cannot minimize app on web');
356
+ return;
357
+ }
358
+ return callNative('minimizeApp');
359
+ },
360
+
361
+ onPause(callback) {
362
+ document.addEventListener('visibilitychange', () => {
363
+ if (document.hidden) callback();
364
+ });
365
+ window.addEventListener('pulse:pause', callback);
366
+ },
367
+
368
+ onResume(callback) {
369
+ document.addEventListener('visibilitychange', () => {
370
+ if (!document.hidden) callback();
371
+ });
372
+ window.addEventListener('pulse:resume', callback);
373
+ },
374
+
375
+ onBackButton(callback) {
376
+ window.addEventListener('pulse:backButton', callback);
377
+ }
378
+ }
379
+ };
380
+
381
+ // =========================================================================
382
+ // CSS for toast animation
383
+ // =========================================================================
384
+ const style = document.createElement('style');
385
+ style.textContent = `
386
+ @keyframes pulse-toast-in {
387
+ from { opacity: 0; transform: translateX(-50%) translateY(20px); }
388
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
389
+ }
390
+ @keyframes pulse-toast-out {
391
+ from { opacity: 1; transform: translateX(-50%) translateY(0); }
392
+ to { opacity: 0; transform: translateX(-50%) translateY(20px); }
393
+ }
394
+ `;
395
+ document.head.appendChild(style);
396
+
397
+ // =========================================================================
398
+ // Initialization
399
+ // =========================================================================
400
+
401
+ window.initPulseNative = function() {
402
+ console.log('[PulseMobile] Initialized on', PulseMobile.platform);
403
+ window.dispatchEvent(new CustomEvent('pulse:ready', {
404
+ detail: { platform: PulseMobile.platform }
405
+ }));
406
+ };
407
+
408
+ // Auto-init on web (native calls this after bridge is ready)
409
+ if (!isNative) {
410
+ if (document.readyState === 'complete') {
411
+ setTimeout(window.initPulseNative, 0);
412
+ } else {
413
+ window.addEventListener('load', () => setTimeout(window.initPulseNative, 0));
414
+ }
415
+ }
416
+
417
+ // Export globally
418
+ window.PulseMobile = PulseMobile;
419
+
420
+ })();
package/package.json CHANGED
@@ -1,58 +1,68 @@
1
- {
2
- "name": "pulse-js-framework",
3
- "version": "1.0.0",
4
- "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
- "type": "module",
6
- "main": "index.js",
7
- "bin": {
8
- "pulse": "cli/index.js"
9
- },
10
- "exports": {
11
- ".": "./index.js",
12
- "./runtime": "./runtime/index.js",
13
- "./runtime/*": "./runtime/*.js",
14
- "./compiler": "./compiler/index.js",
15
- "./vite": "./loader/vite-plugin.js"
16
- },
17
- "files": [
18
- "index.js",
19
- "cli/",
20
- "runtime/",
21
- "compiler/",
22
- "loader/",
23
- "README.md",
24
- "LICENSE"
25
- ],
26
- "scripts": {
27
- "test": "node --test test/*.js"
28
- },
29
- "keywords": [
30
- "framework",
31
- "frontend",
32
- "reactive",
33
- "declarative",
34
- "dom",
35
- "pulse",
36
- "css-selectors",
37
- "dsl",
38
- "ui",
39
- "javascript",
40
- "spa",
41
- "signals"
42
- ],
43
- "author": "Your Name <your.email@example.com>",
44
- "license": "MIT",
45
- "repository": {
46
- "type": "git",
47
- "url": "git+https://github.com/your-username/pulse-framework.git"
48
- },
49
- "homepage": "https://github.com/your-username/pulse-framework#readme",
50
- "bugs": {
51
- "url": "https://github.com/your-username/pulse-framework/issues"
52
- },
53
- "engines": {
54
- "node": ">=18.0.0"
55
- },
56
- "dependencies": {},
57
- "devDependencies": {}
58
- }
1
+ {
2
+ "name": "pulse-js-framework",
3
+ "version": "1.4.0",
4
+ "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "pulse": "cli/index.js"
9
+ },
10
+ "exports": {
11
+ ".": "./index.js",
12
+ "./runtime": "./runtime/index.js",
13
+ "./runtime/*": "./runtime/*.js",
14
+ "./compiler": "./compiler/index.js",
15
+ "./vite": "./loader/vite-plugin.js"
16
+ },
17
+ "files": [
18
+ "index.js",
19
+ "cli/",
20
+ "runtime/",
21
+ "compiler/",
22
+ "loader/",
23
+ "mobile/",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "test": "npm run test:compiler && npm run test:pulse && npm run test:dom && npm run test:lint && npm run test:format && npm run test:analyze",
29
+ "test:compiler": "node test/compiler.test.js",
30
+ "test:pulse": "node test/pulse.test.js",
31
+ "test:dom": "node test/dom.test.js",
32
+ "test:lint": "node test/lint.test.js",
33
+ "test:format": "node test/format.test.js",
34
+ "test:analyze": "node test/analyze.test.js",
35
+ "build:netlify": "node scripts/build-netlify.js"
36
+ },
37
+ "keywords": [
38
+ "framework",
39
+ "frontend",
40
+ "reactive",
41
+ "declarative",
42
+ "dom",
43
+ "pulse",
44
+ "css-selectors",
45
+ "dsl",
46
+ "ui",
47
+ "javascript",
48
+ "spa",
49
+ "signals"
50
+ ],
51
+ "author": "Vincent Hirtz <hirtzvincent@gmail.com>",
52
+ "license": "MIT",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/vincenthirtz/pulse-js-framework.git"
56
+ },
57
+ "homepage": "https://github.com/vincenthirtz/pulse-js-framework#readme",
58
+ "bugs": {
59
+ "url": "https://github.com/vincenthirtz/pulse-js-framework/issues"
60
+ },
61
+ "engines": {
62
+ "node": ">=18.0.0"
63
+ },
64
+ "dependencies": {},
65
+ "devDependencies": {
66
+ "linkedom": "^0.16.8"
67
+ }
68
+ }