canvasframework 0.3.17 → 0.3.19
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/components/Banner.js +342 -0
- package/components/RadioButton.js +17 -9
- package/core/CanvasFramework.js +141 -48
- package/core/UIBuilder.js +2 -0
- package/index.js +11 -1
- package/package.json +1 -1
- package/utils/NotificationManager.js +60 -0
- package/core/CanvasWork.js +0 -32
- package/core/LogicWorker.js +0 -25
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// components/Banner.js
|
|
2
|
+
import Component from '../core/Component.js';
|
|
3
|
+
|
|
4
|
+
export default class Banner extends Component {
|
|
5
|
+
constructor(framework, options = {}) {
|
|
6
|
+
super(framework, options);
|
|
7
|
+
|
|
8
|
+
this.text = options.text || '';
|
|
9
|
+
this.type = options.type || 'info';
|
|
10
|
+
this.actions = options.actions || [];
|
|
11
|
+
this.dismissible = options.dismissible === true;
|
|
12
|
+
|
|
13
|
+
this.platform = framework.platform || 'material';
|
|
14
|
+
|
|
15
|
+
this.width = options.width || framework.width || window.innerWidth;
|
|
16
|
+
this.height = options.height || 64;
|
|
17
|
+
this.x = options.x || 0;
|
|
18
|
+
this.y = options.y || 0;
|
|
19
|
+
|
|
20
|
+
this.visible = options.visible !== false;
|
|
21
|
+
this.progress = this.visible ? 1 : 0;
|
|
22
|
+
this.animSpeed = 0.18;
|
|
23
|
+
|
|
24
|
+
this._lastUpdate = performance.now();
|
|
25
|
+
this._colors = this._resolveColors();
|
|
26
|
+
|
|
27
|
+
// Bounds calculées à chaque frame
|
|
28
|
+
this._actionBounds = [];
|
|
29
|
+
this._dismissBounds = null;
|
|
30
|
+
|
|
31
|
+
// Pour indiquer qu'on gère nos propres clics
|
|
32
|
+
this.selfManagedClicks = true;
|
|
33
|
+
|
|
34
|
+
// Écouter les événements directement sur le canvas
|
|
35
|
+
this._setupEventListeners();
|
|
36
|
+
|
|
37
|
+
// Ref si fourni
|
|
38
|
+
if (options.ref) options.ref.current = this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ===================== Setup ===================== */
|
|
42
|
+
_setupEventListeners() {
|
|
43
|
+
// Stocker les références pour pouvoir les retirer plus tard
|
|
44
|
+
this._boundHandleClick = this._handleClick.bind(this);
|
|
45
|
+
|
|
46
|
+
// Écouter les événements sur le canvas parent
|
|
47
|
+
if (this.framework && this.framework.canvas) {
|
|
48
|
+
this.framework.canvas.addEventListener('click', this._boundHandleClick);
|
|
49
|
+
this.framework.canvas.addEventListener('touchend', this._boundHandleClick);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_removeEventListeners() {
|
|
54
|
+
if (this.framework && this.framework.canvas && this._boundHandleClick) {
|
|
55
|
+
this.framework.canvas.removeEventListener('click', this._boundHandleClick);
|
|
56
|
+
this.framework.canvas.removeEventListener('touchend', this._boundHandleClick);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ===================== Lifecycle ===================== */
|
|
61
|
+
onMount() {
|
|
62
|
+
this._setupEventListeners();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onUnmount() {
|
|
66
|
+
this._removeEventListeners();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ===================== Colors ===================== */
|
|
70
|
+
_resolveColors() {
|
|
71
|
+
if (this.platform === 'cupertino') {
|
|
72
|
+
return {
|
|
73
|
+
bg: 'rgba(250,250,250,0.95)',
|
|
74
|
+
fg: '#000',
|
|
75
|
+
accent: '#007AFF',
|
|
76
|
+
divider: 'rgba(60,60,67,0.15)'
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Material v3
|
|
81
|
+
const map = {
|
|
82
|
+
info: '#E8F0FE',
|
|
83
|
+
success: '#E6F4EA',
|
|
84
|
+
warning: '#FEF7E0',
|
|
85
|
+
error: '#FCE8E6'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
bg: map[this.type] || map.info,
|
|
90
|
+
fg: '#1F1F1F',
|
|
91
|
+
accent: '#1A73E8'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ===================== Show/Hide ===================== */
|
|
96
|
+
show() {
|
|
97
|
+
this.visible = true;
|
|
98
|
+
this.markDirty();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
hide() {
|
|
102
|
+
this.visible = false;
|
|
103
|
+
this.markDirty();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ===================== Update ===================== */
|
|
107
|
+
update() {
|
|
108
|
+
const now = performance.now();
|
|
109
|
+
const dt = Math.min((now - this._lastUpdate) / 16.6, 3);
|
|
110
|
+
|
|
111
|
+
const target = this.visible ? 1 : 0;
|
|
112
|
+
this.progress += (target - this.progress) * this.animSpeed * dt;
|
|
113
|
+
this.progress = Math.max(0, Math.min(1, this.progress));
|
|
114
|
+
|
|
115
|
+
if (Math.abs(target - this.progress) > 0.01) this.markDirty();
|
|
116
|
+
|
|
117
|
+
this._lastUpdate = now;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ===================== Draw ===================== */
|
|
121
|
+
draw(ctx) {
|
|
122
|
+
this.update();
|
|
123
|
+
if (this.progress <= 0.01) return;
|
|
124
|
+
|
|
125
|
+
const h = this.height * this.progress;
|
|
126
|
+
const visibleHeight = h;
|
|
127
|
+
|
|
128
|
+
ctx.save();
|
|
129
|
+
|
|
130
|
+
// Background
|
|
131
|
+
if (this.platform === 'material') {
|
|
132
|
+
ctx.shadowColor = 'rgba(0,0,0,0.18)';
|
|
133
|
+
ctx.shadowBlur = 8;
|
|
134
|
+
ctx.shadowOffsetY = 2;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
ctx.fillStyle = this._colors.bg;
|
|
138
|
+
ctx.fillRect(this.x, this.y, this.width, visibleHeight);
|
|
139
|
+
ctx.shadowColor = 'transparent';
|
|
140
|
+
|
|
141
|
+
// Divider iOS
|
|
142
|
+
if (this.platform === 'cupertino') {
|
|
143
|
+
ctx.strokeStyle = this._colors.divider;
|
|
144
|
+
ctx.beginPath();
|
|
145
|
+
ctx.moveTo(this.x, this.y + visibleHeight);
|
|
146
|
+
ctx.lineTo(this.x + this.width, this.y + visibleHeight);
|
|
147
|
+
ctx.stroke();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Text
|
|
151
|
+
ctx.fillStyle = this._colors.fg;
|
|
152
|
+
ctx.font =
|
|
153
|
+
this.platform === 'cupertino'
|
|
154
|
+
? '400 15px -apple-system'
|
|
155
|
+
: '400 14px Roboto, sans-serif';
|
|
156
|
+
ctx.textBaseline = 'middle';
|
|
157
|
+
ctx.textAlign = 'left';
|
|
158
|
+
ctx.fillText(this.text, this.x + 16, this.y + visibleHeight / 2);
|
|
159
|
+
|
|
160
|
+
// Actions - calculer et stocker les bounds
|
|
161
|
+
this._actionBounds = [];
|
|
162
|
+
let x = this.width - 16;
|
|
163
|
+
|
|
164
|
+
for (let i = this.actions.length - 1; i >= 0; i--) {
|
|
165
|
+
const action = this.actions[i];
|
|
166
|
+
const textWidth = ctx.measureText(action.label).width + 20;
|
|
167
|
+
x -= textWidth;
|
|
168
|
+
|
|
169
|
+
ctx.fillStyle = this._colors.accent;
|
|
170
|
+
ctx.textAlign = 'center';
|
|
171
|
+
ctx.textBaseline = 'middle';
|
|
172
|
+
ctx.fillText(action.label, this.x + x + textWidth / 2, this.y + visibleHeight / 2);
|
|
173
|
+
|
|
174
|
+
// Stocker la hitbox (en coordonnées écran, pas canvas)
|
|
175
|
+
this._actionBounds.push({
|
|
176
|
+
action: action,
|
|
177
|
+
bounds: {
|
|
178
|
+
x: this.x + x,
|
|
179
|
+
y: this.y + (visibleHeight - 44) / 2,
|
|
180
|
+
w: textWidth,
|
|
181
|
+
h: 44
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
x -= 12;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Dismiss button
|
|
189
|
+
if (this.dismissible) {
|
|
190
|
+
const hitSize = 44;
|
|
191
|
+
const cx = this.width - 28;
|
|
192
|
+
const cy = this.y + visibleHeight / 2;
|
|
193
|
+
|
|
194
|
+
ctx.fillStyle =
|
|
195
|
+
this.platform === 'cupertino'
|
|
196
|
+
? 'rgba(60,60,67,0.6)'
|
|
197
|
+
: this._colors.fg;
|
|
198
|
+
|
|
199
|
+
ctx.font =
|
|
200
|
+
this.platform === 'cupertino'
|
|
201
|
+
? '600 16px -apple-system'
|
|
202
|
+
: '500 16px Roboto';
|
|
203
|
+
ctx.textAlign = 'center';
|
|
204
|
+
ctx.textBaseline = 'middle';
|
|
205
|
+
ctx.fillText('×', cx, cy);
|
|
206
|
+
|
|
207
|
+
this._dismissBounds = {
|
|
208
|
+
x: cx - hitSize / 2,
|
|
209
|
+
y: cy - hitSize / 2,
|
|
210
|
+
w: hitSize,
|
|
211
|
+
h: hitSize
|
|
212
|
+
};
|
|
213
|
+
} else {
|
|
214
|
+
this._dismissBounds = null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
ctx.restore();
|
|
218
|
+
|
|
219
|
+
// DEBUG: Dessiner les hitboxes
|
|
220
|
+
if (this.framework && this.framework.debbug) {
|
|
221
|
+
this._drawDebugHitboxes(ctx);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ===================== Debug ===================== */
|
|
226
|
+
_drawDebugHitboxes(ctx) {
|
|
227
|
+
ctx.save();
|
|
228
|
+
ctx.strokeStyle = 'red';
|
|
229
|
+
ctx.lineWidth = 1;
|
|
230
|
+
ctx.fillStyle = 'rgba(255, 0, 0, 0.1)';
|
|
231
|
+
|
|
232
|
+
// Dessiner la hitbox principale du banner
|
|
233
|
+
const h = this.height * this.progress;
|
|
234
|
+
ctx.strokeRect(this.x, this.y, this.width, h);
|
|
235
|
+
|
|
236
|
+
// Dessiner les hitboxes des actions
|
|
237
|
+
if (this._actionBounds && this._actionBounds.length > 0) {
|
|
238
|
+
for (const item of this._actionBounds) {
|
|
239
|
+
const b = item.bounds;
|
|
240
|
+
ctx.fillRect(b.x, b.y, b.w, b.h);
|
|
241
|
+
ctx.strokeRect(b.x, b.y, b.w, b.h);
|
|
242
|
+
|
|
243
|
+
// Texte de debug
|
|
244
|
+
ctx.fillStyle = 'red';
|
|
245
|
+
ctx.font = '10px monospace';
|
|
246
|
+
ctx.fillText(item.action.label, b.x + 5, b.y + 12);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Dessiner la hitbox du dismiss button
|
|
251
|
+
if (this._dismissBounds) {
|
|
252
|
+
const b = this._dismissBounds;
|
|
253
|
+
ctx.fillRect(b.x, b.y, b.w, b.h);
|
|
254
|
+
ctx.strokeRect(b.x, b.y, b.w, b.h);
|
|
255
|
+
ctx.fillText('X', b.x + 5, b.y + 12);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
ctx.restore();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* ===================== Click Handling ===================== */
|
|
262
|
+
_handleClick(event) {
|
|
263
|
+
if (this.progress < 0.95) return;
|
|
264
|
+
|
|
265
|
+
// Obtenir les coordonnées du clic/touch
|
|
266
|
+
let clientX, clientY;
|
|
267
|
+
|
|
268
|
+
if (event.type === 'touchend') {
|
|
269
|
+
const touch = event.changedTouches[0];
|
|
270
|
+
clientX = touch.clientX;
|
|
271
|
+
clientY = touch.clientY;
|
|
272
|
+
} else {
|
|
273
|
+
clientX = event.clientX;
|
|
274
|
+
clientY = event.clientY;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Convertir en coordonnées canvas SIMPLIFIÉ
|
|
278
|
+
const canvasRect = this.framework.canvas.getBoundingClientRect();
|
|
279
|
+
|
|
280
|
+
// Coordonnées relatives au canvas (en pixels CSS, pas en pixels canvas)
|
|
281
|
+
const x = clientX - canvasRect.left;
|
|
282
|
+
const y = clientY - canvasRect.top;
|
|
283
|
+
|
|
284
|
+
console.log('Click converted:', {
|
|
285
|
+
clientX, clientY,
|
|
286
|
+
canvasLeft: canvasRect.left,
|
|
287
|
+
canvasTop: canvasRect.top,
|
|
288
|
+
x, y,
|
|
289
|
+
bannerX: this.x,
|
|
290
|
+
bannerY: this.y,
|
|
291
|
+
bannerWidth: this.width,
|
|
292
|
+
bannerHeight: this.height * this.progress
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Vérifier si on clique sur le banner (en coordonnées CSS)
|
|
296
|
+
const bannerBottom = this.y + (this.height * this.progress);
|
|
297
|
+
if (x < this.x || x > this.x + this.width || y < this.y || y > bannerBottom) {
|
|
298
|
+
console.log('Click outside banner');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log('Click INSIDE banner!');
|
|
303
|
+
|
|
304
|
+
// Empêcher la propagation
|
|
305
|
+
event.stopPropagation();
|
|
306
|
+
|
|
307
|
+
// 1️⃣ Dismiss button
|
|
308
|
+
if (this.dismissible && this._dismissBounds) {
|
|
309
|
+
const b = this._dismissBounds;
|
|
310
|
+
console.log('Checking dismiss bounds:', b, 'click:', {x, y});
|
|
311
|
+
if (x >= b.x && x <= b.x + b.w &&
|
|
312
|
+
y >= b.y && y <= b.y + b.h) {
|
|
313
|
+
console.log('Dismiss clicked!');
|
|
314
|
+
this.hide();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 2️⃣ Actions
|
|
320
|
+
if (this._actionBounds && this._actionBounds.length > 0) {
|
|
321
|
+
console.log('Checking', this._actionBounds.length, 'action bounds');
|
|
322
|
+
for (const item of this._actionBounds) {
|
|
323
|
+
const b = item.bounds;
|
|
324
|
+
console.log('Checking action:', item.action.label, 'bounds:', b);
|
|
325
|
+
if (x >= b.x && x <= b.x + b.w &&
|
|
326
|
+
y >= b.y && y <= b.y + b.h) {
|
|
327
|
+
console.log('Action clicked:', item.action.label);
|
|
328
|
+
item.action.onClick?.();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log('Click on banner but not on any button');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* ===================== Resize ===================== */
|
|
338
|
+
_resize(width) {
|
|
339
|
+
this.width = width;
|
|
340
|
+
this.markDirty();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -97,19 +97,27 @@ class RadioButton extends Component {
|
|
|
97
97
|
ctx.arc(centerX, centerY, 5, 0, Math.PI * 2);
|
|
98
98
|
ctx.fill();
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
// Cupertino
|
|
102
|
-
ctx.strokeStyle = this.checked ? '#007AFF' : '#C7C7CC';
|
|
103
|
-
ctx.lineWidth = 2;
|
|
104
|
-
ctx.beginPath();
|
|
105
|
-
ctx.arc(centerX, centerY, this.circleRadius, 0, Math.PI * 2);
|
|
106
|
-
ctx.stroke();
|
|
107
|
-
|
|
100
|
+
} else {
|
|
101
|
+
// Cupertino (iOS style)
|
|
108
102
|
if (this.checked) {
|
|
103
|
+
// Cercle bleu rempli
|
|
109
104
|
ctx.fillStyle = '#007AFF';
|
|
110
105
|
ctx.beginPath();
|
|
111
|
-
ctx.arc(centerX, centerY,
|
|
106
|
+
ctx.arc(centerX, centerY, this.circleRadius, 0, Math.PI * 2);
|
|
107
|
+
ctx.fill();
|
|
108
|
+
|
|
109
|
+
// Point blanc au centre
|
|
110
|
+
ctx.fillStyle = '#FFFFFF';
|
|
111
|
+
ctx.beginPath();
|
|
112
|
+
ctx.arc(centerX, centerY, 4, 0, Math.PI * 2);
|
|
112
113
|
ctx.fill();
|
|
114
|
+
} else {
|
|
115
|
+
// Cercle gris clair
|
|
116
|
+
ctx.strokeStyle = '#D1D1D6';
|
|
117
|
+
ctx.lineWidth = 1.5;
|
|
118
|
+
ctx.beginPath();
|
|
119
|
+
ctx.arc(centerX, centerY, this.circleRadius, 0, Math.PI * 2);
|
|
120
|
+
ctx.stroke();
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
123
|
|
package/core/CanvasFramework.js
CHANGED
|
@@ -50,6 +50,7 @@ import ImageCarousel from '../components/ImageCarousel.js';
|
|
|
50
50
|
import PasswordInput from '../components/PasswordInput.js';
|
|
51
51
|
import InputTags from '../components/InputTags.js';
|
|
52
52
|
import InputDatalist from '../components/InputDatalist.js';
|
|
53
|
+
import Banner from '../components/Banner.js';
|
|
53
54
|
|
|
54
55
|
// Utils
|
|
55
56
|
import SafeArea from '../utils/SafeArea.js';
|
|
@@ -67,6 +68,7 @@ import GeoLocationService from '../utils/GeoLocationService.js';
|
|
|
67
68
|
import WebSocketClient from '../utils/WebSocketClient.js';
|
|
68
69
|
import AnimationEngine from '../utils/AnimationEngine.js';
|
|
69
70
|
import CryptoManager from '../utils/CryptoManager.js';
|
|
71
|
+
import NotificationManager from '../utils/NotificationManager.js';
|
|
70
72
|
|
|
71
73
|
// Features
|
|
72
74
|
import PullToRefresh from '../features/PullToRefresh.js';
|
|
@@ -113,6 +115,21 @@ export const darkTheme = {
|
|
|
113
115
|
border: '#333333'
|
|
114
116
|
};
|
|
115
117
|
|
|
118
|
+
const FIXED_COMPONENT_TYPES = new Set([
|
|
119
|
+
AppBar,
|
|
120
|
+
BottomNavigationBar,
|
|
121
|
+
Drawer,
|
|
122
|
+
Dialog,
|
|
123
|
+
Modal,
|
|
124
|
+
FAB,
|
|
125
|
+
Toast,
|
|
126
|
+
Banner,
|
|
127
|
+
BottomSheet,
|
|
128
|
+
ContextMenu,
|
|
129
|
+
OpenStreetMap,
|
|
130
|
+
SelectDialog
|
|
131
|
+
]);
|
|
132
|
+
|
|
116
133
|
/**
|
|
117
134
|
* Framework principal pour créer des interfaces utilisateur basées sur Canvas
|
|
118
135
|
* @class
|
|
@@ -172,15 +189,17 @@ class CanvasFramework {
|
|
|
172
189
|
this._lastFpsTime = performance.now();
|
|
173
190
|
this.showFps = options.showFps || false; // false par défaut
|
|
174
191
|
this.debbug = options.debug || false; // false par défaut (et correction de la faute de frappe)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.worker
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
192
|
+
|
|
193
|
+
// Worker pour multithreading Canvas Worker
|
|
194
|
+
this.worker = this.createCanvasWorker();
|
|
195
|
+
this.worker.onmessage = this.handleWorkerMessage.bind(this);
|
|
196
|
+
this.worker.postMessage({ type: 'INIT', payload: { components: [] } });
|
|
197
|
+
|
|
198
|
+
// Logic Worker
|
|
199
|
+
this.logicWorker = this.createLogicWorker();
|
|
200
|
+
this.logicWorker.onmessage = this.handleLogicWorkerMessage.bind(this);
|
|
201
|
+
this.logicWorkerState = {};
|
|
202
|
+
this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.state });
|
|
184
203
|
|
|
185
204
|
// Envoyer l'état initial au worker
|
|
186
205
|
this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.state });
|
|
@@ -238,6 +257,74 @@ class CanvasFramework {
|
|
|
238
257
|
get: () => originalFillStyle.get.call(ctx)
|
|
239
258
|
});
|
|
240
259
|
}
|
|
260
|
+
|
|
261
|
+
createCanvasWorker() {
|
|
262
|
+
const workerCode = `
|
|
263
|
+
let components = [];
|
|
264
|
+
|
|
265
|
+
self.onmessage = function(e) {
|
|
266
|
+
const { type, payload } = e.data;
|
|
267
|
+
|
|
268
|
+
switch(type) {
|
|
269
|
+
case 'INIT':
|
|
270
|
+
components = payload.components;
|
|
271
|
+
self.postMessage({ type: 'READY' });
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
case 'UPDATE_LAYOUT':
|
|
275
|
+
const updated = components.map(comp => {
|
|
276
|
+
if (comp.dynamicHeight && comp.calculateHeight) {
|
|
277
|
+
comp.height = comp.calculateHeight();
|
|
278
|
+
}
|
|
279
|
+
return { id: comp.id, height: comp.height };
|
|
280
|
+
});
|
|
281
|
+
self.postMessage({ type: 'LAYOUT_DONE', payload: updated });
|
|
282
|
+
break;
|
|
283
|
+
|
|
284
|
+
case 'SCROLL_INERTIA':
|
|
285
|
+
let { offset, velocity, friction, maxScroll } = payload;
|
|
286
|
+
offset += velocity;
|
|
287
|
+
offset = Math.max(Math.min(offset, 0), -maxScroll);
|
|
288
|
+
velocity *= friction;
|
|
289
|
+
self.postMessage({ type: 'SCROLL_UPDATED', payload: { offset, velocity } });
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
`;
|
|
294
|
+
|
|
295
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
296
|
+
return new Worker(URL.createObjectURL(blob));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
createLogicWorker() {
|
|
300
|
+
const workerCode = `
|
|
301
|
+
let state = {};
|
|
302
|
+
|
|
303
|
+
self.onmessage = async function(e) {
|
|
304
|
+
const { type, payload } = e.data;
|
|
305
|
+
|
|
306
|
+
switch(type) {
|
|
307
|
+
case 'SET_STATE':
|
|
308
|
+
state = payload;
|
|
309
|
+
self.postMessage({ type: 'STATE_UPDATED', payload: state });
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case 'EXECUTE':
|
|
313
|
+
try {
|
|
314
|
+
const fn = new Function('state', 'args', payload.fnString);
|
|
315
|
+
const result = await fn(state, payload.args);
|
|
316
|
+
self.postMessage({ type: 'EXECUTION_RESULT', payload: result });
|
|
317
|
+
} catch (err) {
|
|
318
|
+
self.postMessage({ type: 'EXECUTION_ERROR', payload: err.message });
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
326
|
+
return new Worker(URL.createObjectURL(blob));
|
|
327
|
+
}
|
|
241
328
|
|
|
242
329
|
// Set Theme dynamique
|
|
243
330
|
setTheme(theme) {
|
|
@@ -819,19 +906,8 @@ class CanvasFramework {
|
|
|
819
906
|
}
|
|
820
907
|
|
|
821
908
|
checkComponentsAtPosition(x, y, eventType) {
|
|
822
|
-
const isFixedComponent = (comp) =>
|
|
823
|
-
|
|
824
|
-
comp instanceof BottomNavigationBar ||
|
|
825
|
-
comp instanceof Drawer ||
|
|
826
|
-
comp instanceof Dialog ||
|
|
827
|
-
comp instanceof Modal ||
|
|
828
|
-
comp instanceof FAB ||
|
|
829
|
-
comp instanceof Toast ||
|
|
830
|
-
comp instanceof BottomSheet ||
|
|
831
|
-
comp instanceof ContextMenu ||
|
|
832
|
-
comp instanceof OpenStreetMap ||
|
|
833
|
-
comp instanceof SelectDialog;
|
|
834
|
-
};
|
|
909
|
+
const isFixedComponent = (comp) =>
|
|
910
|
+
FIXED_COMPONENT_TYPES.has(comp.constructor);
|
|
835
911
|
|
|
836
912
|
for (let i = this.components.length - 1; i >= 0; i--) {
|
|
837
913
|
const comp = this.components[i];
|
|
@@ -859,7 +935,7 @@ class CanvasFramework {
|
|
|
859
935
|
switch (eventType) {
|
|
860
936
|
case 'start':
|
|
861
937
|
child.pressed = true;
|
|
862
|
-
if (child.onPress) child.onPress(relativeX, relativeY);
|
|
938
|
+
if (child.onPress) child.onPress?.(relativeX, relativeY);
|
|
863
939
|
break;
|
|
864
940
|
|
|
865
941
|
case 'move':
|
|
@@ -867,7 +943,7 @@ class CanvasFramework {
|
|
|
867
943
|
child.hovered = true;
|
|
868
944
|
if (child.onHover) child.onHover();
|
|
869
945
|
}
|
|
870
|
-
if (child.onMove) child.onMove(relativeX, relativeY);
|
|
946
|
+
if (child.onMove) child.onMove?.(relativeX, relativeY);
|
|
871
947
|
break;
|
|
872
948
|
|
|
873
949
|
case 'end':
|
|
@@ -876,11 +952,18 @@ class CanvasFramework {
|
|
|
876
952
|
|
|
877
953
|
if (child instanceof Input || child instanceof PasswordInput || child instanceof InputTags || child instanceof InputDatalist) {
|
|
878
954
|
for (let other of this.components) {
|
|
879
|
-
if (
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
955
|
+
if (
|
|
956
|
+
(other instanceof Input ||
|
|
957
|
+
other instanceof PasswordInput ||
|
|
958
|
+
other instanceof InputTags ||
|
|
959
|
+
other instanceof InputDatalist) &&
|
|
960
|
+
other !== child &&
|
|
961
|
+
other.focused
|
|
962
|
+
) {
|
|
963
|
+
other.focused = false;
|
|
964
|
+
other.cursorVisible = false;
|
|
965
|
+
other.onBlur?.();
|
|
966
|
+
}
|
|
884
967
|
}
|
|
885
968
|
|
|
886
969
|
child.focused = true;
|
|
@@ -889,7 +972,7 @@ class CanvasFramework {
|
|
|
889
972
|
} else if (child.onClick) {
|
|
890
973
|
child.onClick();
|
|
891
974
|
} else if (child.onPress) {
|
|
892
|
-
child.onPress(relativeX, relativeY);
|
|
975
|
+
child.onPress?.(relativeX, relativeY);
|
|
893
976
|
}
|
|
894
977
|
}
|
|
895
978
|
break;
|
|
@@ -922,11 +1005,18 @@ class CanvasFramework {
|
|
|
922
1005
|
|
|
923
1006
|
if (comp instanceof Input || comp instanceof PasswordInput || comp instanceof InputTags || comp instanceof InputDatalist) {
|
|
924
1007
|
for (let other of this.components) {
|
|
925
|
-
if (
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1008
|
+
if (
|
|
1009
|
+
(other instanceof Input ||
|
|
1010
|
+
other instanceof PasswordInput ||
|
|
1011
|
+
other instanceof InputTags ||
|
|
1012
|
+
other instanceof InputDatalist) &&
|
|
1013
|
+
other !== comp &&
|
|
1014
|
+
other.focused
|
|
1015
|
+
) {
|
|
1016
|
+
other.focused = false;
|
|
1017
|
+
other.cursorVisible = false;
|
|
1018
|
+
other.onBlur?.();
|
|
1019
|
+
}
|
|
930
1020
|
}
|
|
931
1021
|
|
|
932
1022
|
comp.focused = true;
|
|
@@ -948,8 +1038,18 @@ class CanvasFramework {
|
|
|
948
1038
|
}
|
|
949
1039
|
}
|
|
950
1040
|
}
|
|
951
|
-
|
|
1041
|
+
|
|
952
1042
|
getMaxScroll() {
|
|
1043
|
+
let maxY = 0;
|
|
1044
|
+
for (const comp of this.components) {
|
|
1045
|
+
if (this.isFixedComponent(comp) || !comp.visible) continue;
|
|
1046
|
+
const bottom = comp.y + comp.height;
|
|
1047
|
+
if (bottom > maxY) maxY = bottom;
|
|
1048
|
+
}
|
|
1049
|
+
return Math.max(0, maxY - this.height + 50);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/*getMaxScroll() {
|
|
953
1053
|
let maxY = 0;
|
|
954
1054
|
for (let comp of this.components) {
|
|
955
1055
|
if (!this.isFixedComponent(comp)) {
|
|
@@ -957,7 +1057,7 @@ class CanvasFramework {
|
|
|
957
1057
|
}
|
|
958
1058
|
}
|
|
959
1059
|
return Math.max(0, maxY - this.height + 50);
|
|
960
|
-
}
|
|
1060
|
+
}*/
|
|
961
1061
|
|
|
962
1062
|
handleResize() {
|
|
963
1063
|
// Pour WebGL, NE PAS redimensionner automatiquement
|
|
@@ -1251,17 +1351,7 @@ class CanvasFramework {
|
|
|
1251
1351
|
}
|
|
1252
1352
|
|
|
1253
1353
|
isFixedComponent(comp) {
|
|
1254
|
-
|
|
1255
|
-
comp instanceof BottomNavigationBar ||
|
|
1256
|
-
comp instanceof Drawer ||
|
|
1257
|
-
comp instanceof Dialog ||
|
|
1258
|
-
comp instanceof Modal ||
|
|
1259
|
-
comp instanceof FAB ||
|
|
1260
|
-
comp instanceof Toast ||
|
|
1261
|
-
comp instanceof BottomSheet ||
|
|
1262
|
-
comp instanceof ContextMenu ||
|
|
1263
|
-
comp instanceof OpenStreetMap ||
|
|
1264
|
-
comp instanceof SelectDialog;
|
|
1354
|
+
return FIXED_COMPONENT_TYPES.has(comp.constructor);
|
|
1265
1355
|
}
|
|
1266
1356
|
|
|
1267
1357
|
showToast(message, duration = 3000) {
|
|
@@ -1277,3 +1367,6 @@ class CanvasFramework {
|
|
|
1277
1367
|
}
|
|
1278
1368
|
|
|
1279
1369
|
export default CanvasFramework;
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
|
package/core/UIBuilder.js
CHANGED
|
@@ -51,6 +51,7 @@ import ImageCarousel from '../components/ImageCarousel.js';
|
|
|
51
51
|
import PasswordInput from '../components/PasswordInput.js';
|
|
52
52
|
import InputTags from '../components/InputTags.js';
|
|
53
53
|
import InputDatalist from '../components/InputDatalist.js';
|
|
54
|
+
import Banner from '../components/Banner.js';
|
|
54
55
|
|
|
55
56
|
// Features
|
|
56
57
|
import PullToRefresh from '../features/PullToRefresh.js';
|
|
@@ -129,6 +130,7 @@ const Components = {
|
|
|
129
130
|
Row,
|
|
130
131
|
Column,
|
|
131
132
|
Positioned,
|
|
133
|
+
Banner,
|
|
132
134
|
Stack
|
|
133
135
|
};
|
|
134
136
|
|
package/index.js
CHANGED
|
@@ -58,6 +58,7 @@ export { default as ImageCarousel } from './components/ImageCarousel.js';
|
|
|
58
58
|
export { default as PasswordInput } from './components/PasswordInput.js';
|
|
59
59
|
export { default as InputTags } from './components/InputTags.js';
|
|
60
60
|
export { default as InputDatalist } from './components/InputDatalist.js';
|
|
61
|
+
export { default as Banner } from './components/Banner.js';
|
|
61
62
|
|
|
62
63
|
// Utils
|
|
63
64
|
export { default as SafeArea } from './utils/SafeArea.js';
|
|
@@ -75,6 +76,7 @@ export { default as GeoLocationService } from './utils/GeoLocationService.js';
|
|
|
75
76
|
export { default as WebSocketClient } from './utils/WebSocketClient.js';
|
|
76
77
|
export { default as AnimationEngine } from './utils/AnimationEngine.js';
|
|
77
78
|
export { default as CryptoManager } from './utils/CryptoManager.js';
|
|
79
|
+
export { default as NotificationManager } from './utils/NotificationManager.js';
|
|
78
80
|
|
|
79
81
|
// Features
|
|
80
82
|
export { default as PullToRefresh } from './features/PullToRefresh.js';
|
|
@@ -98,7 +100,15 @@ export { default as FeatureFlags } from './manager/FeatureFlags.js';
|
|
|
98
100
|
|
|
99
101
|
// Version du framework
|
|
100
102
|
|
|
101
|
-
export const VERSION = '0.3.
|
|
103
|
+
export const VERSION = '0.3.19';
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
102
112
|
|
|
103
113
|
|
|
104
114
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
|
|
2
|
+
// NotificationManager.js
|
|
3
|
+
export default class NotificationManager {
|
|
4
|
+
constructor(defaults = {}) {
|
|
5
|
+
this.defaults = {
|
|
6
|
+
icon: defaults.icon || null,
|
|
7
|
+
silent: defaults.silent || false,
|
|
8
|
+
requireInteraction: defaults.requireInteraction || false,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Vérifier si l'API est disponible
|
|
12
|
+
this.isSupported = "Notification" in window;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Demander la permission si nécessaire
|
|
16
|
+
async requestPermission() {
|
|
17
|
+
if (!this.isSupported) return false;
|
|
18
|
+
if (Notification.permission === "granted") return true;
|
|
19
|
+
if (Notification.permission !== "denied") {
|
|
20
|
+
const permission = await Notification.requestPermission();
|
|
21
|
+
return permission === "granted";
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Créer une notification
|
|
27
|
+
async notify(title, options = {}) {
|
|
28
|
+
if (!this.isSupported) {
|
|
29
|
+
console.warn("Notifications API non supportée");
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hasPermission = await this.requestPermission();
|
|
34
|
+
if (!hasPermission) return null;
|
|
35
|
+
|
|
36
|
+
const notifOptions = {
|
|
37
|
+
...this.defaults,
|
|
38
|
+
...options,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const notification = new Notification(title, notifOptions);
|
|
42
|
+
|
|
43
|
+
// Callbacks
|
|
44
|
+
if (options.onClick) {
|
|
45
|
+
notification.onclick = options.onClick;
|
|
46
|
+
}
|
|
47
|
+
if (options.onClose) {
|
|
48
|
+
notification.onclose = options.onClose;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Auto-close après duration (si défini)
|
|
52
|
+
if (options.duration && options.duration > 0) {
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
notification.close();
|
|
55
|
+
}, options.duration);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return notification;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/core/CanvasWork.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// CanvasWorker.js
|
|
2
|
-
let components = [];
|
|
3
|
-
|
|
4
|
-
self.onmessage = (e) => {
|
|
5
|
-
const { type, payload } = e.data;
|
|
6
|
-
|
|
7
|
-
switch(type) {
|
|
8
|
-
case 'INIT':
|
|
9
|
-
components = payload.components;
|
|
10
|
-
self.postMessage({ type: 'READY' });
|
|
11
|
-
break;
|
|
12
|
-
|
|
13
|
-
case 'UPDATE_LAYOUT':
|
|
14
|
-
// Recalculer la hauteur des composants dynamiques
|
|
15
|
-
const updated = components.map(comp => {
|
|
16
|
-
if (comp.dynamicHeight && comp.calculateHeight) {
|
|
17
|
-
comp.height = comp.calculateHeight();
|
|
18
|
-
}
|
|
19
|
-
return { id: comp.id, height: comp.height };
|
|
20
|
-
});
|
|
21
|
-
self.postMessage({ type: 'LAYOUT_DONE', payload: updated });
|
|
22
|
-
break;
|
|
23
|
-
|
|
24
|
-
case 'SCROLL_INERTIA':
|
|
25
|
-
let { offset, velocity, friction, maxScroll } = payload;
|
|
26
|
-
offset += velocity;
|
|
27
|
-
offset = Math.max(Math.min(offset, 0), -maxScroll);
|
|
28
|
-
velocity *= friction;
|
|
29
|
-
self.postMessage({ type: 'SCROLL_UPDATED', payload: { offset, velocity } });
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
};
|
package/core/LogicWorker.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// LogicWorker.js
|
|
2
|
-
let state = {};
|
|
3
|
-
|
|
4
|
-
self.onmessage = async (e) => {
|
|
5
|
-
const { type, payload } = e.data;
|
|
6
|
-
|
|
7
|
-
switch(type) {
|
|
8
|
-
case 'SET_STATE':
|
|
9
|
-
state = payload;
|
|
10
|
-
self.postMessage({ type: 'STATE_UPDATED', payload: state });
|
|
11
|
-
break;
|
|
12
|
-
|
|
13
|
-
case 'EXECUTE':
|
|
14
|
-
// payload: { fnString: string, args: array }
|
|
15
|
-
// Attention : on envoie la fonction en string et on l'exécute ici
|
|
16
|
-
try {
|
|
17
|
-
const fn = new Function('state', 'args', payload.fnString);
|
|
18
|
-
const result = await fn(state, payload.args);
|
|
19
|
-
self.postMessage({ type: 'EXECUTION_RESULT', payload: result });
|
|
20
|
-
} catch (err) {
|
|
21
|
-
self.postMessage({ type: 'EXECUTION_ERROR', payload: err.message });
|
|
22
|
-
}
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
25
|
-
};
|