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.
@@ -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
- } else {
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, 5, 0, Math.PI * 2);
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
 
@@ -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
- // Worker pour multithreading
176
- this.worker = new Worker('./CanvasWorker.js', { type: 'module' });
177
- this.worker.onmessage = this.handleWorkerMessage.bind(this);
178
- this.worker.postMessage({ type: 'INIT', payload: { components: [] } });
179
-
180
- // Worker logique pour calculs séparés
181
- this.logicWorker = new Worker('./LogicWorker.js', { type: 'module' });
182
- this.logicWorker.onmessage = this.handleLogicWorkerMessage.bind(this);
183
- this.logicWorkerState = {};
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
- return comp instanceof AppBar ||
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 (other instanceof Input || other instanceof PasswordInput || other instanceof InputTags || other instanceof InputDatalist && other !== child && other.focused) {
880
- other.focused = false;
881
- other.cursorVisible = false;
882
- if (other.onBlur) other.onBlur();
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 (other instanceof Input || other instanceof PasswordInput || other instanceof InputTags || other instanceof InputDatalist && other !== comp && other.focused) {
926
- other.focused = false;
927
- other.cursorVisible = false;
928
- if (other.onBlur) other.onBlur();
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
- return comp instanceof AppBar ||
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.16';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -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
+ }
@@ -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
- };
@@ -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
- };