canvasframework 0.3.18 → 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';
@@ -122,6 +123,7 @@ const FIXED_COMPONENT_TYPES = new Set([
122
123
  Modal,
123
124
  FAB,
124
125
  Toast,
126
+ Banner,
125
127
  BottomSheet,
126
128
  ContextMenu,
127
129
  OpenStreetMap,
@@ -187,16 +189,17 @@ class CanvasFramework {
187
189
  this._lastFpsTime = performance.now();
188
190
  this.showFps = options.showFps || false; // false par défaut
189
191
  this.debbug = options.debug || false; // false par défaut (et correction de la faute de frappe)
190
- // Worker pour multithreading
191
- this.worker = new Worker(new URL('./CanvasWorker.js', import.meta.url), { type: 'module' });
192
- this.worker.onmessage = this.handleWorkerMessage.bind(this);
193
- this.worker.postMessage({ type: 'INIT', payload: { components: [] } });
194
-
195
- // Worker logique pour calculs séparés
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: [] } });
196
197
 
197
- this.logicWorker = new Worker(new URL('./LogicWorker.js', import.meta.url), { type: 'module' });
198
- this.logicWorker.onmessage = this.handleLogicWorkerMessage.bind(this);
199
- this.logicWorkerState = {};
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 });
200
203
 
201
204
  // Envoyer l'état initial au worker
202
205
  this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.state });
@@ -254,6 +257,74 @@ class CanvasFramework {
254
257
  get: () => originalFillStyle.get.call(ctx)
255
258
  });
256
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
+ }
257
328
 
258
329
  // Set Theme dynamique
259
330
  setTheme(theme) {
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';
@@ -99,7 +100,7 @@ export { default as FeatureFlags } from './manager/FeatureFlags.js';
99
100
 
100
101
  // Version du framework
101
102
 
102
- export const VERSION = '0.3.18';
103
+ export const VERSION = '0.3.19';
103
104
 
104
105
 
105
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.3.18",
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",
@@ -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
- };