canvasframework 0.5.58 → 0.5.60

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,494 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Système de notation par étoiles avec variantes Material et Cupertino
5
+ * @class
6
+ * @extends Component
7
+ *
8
+ * Material: Étoiles avec animation ripple
9
+ * Cupertino: Étoiles iOS style SF Symbols
10
+ */
11
+ class Rating extends Component {
12
+ /**
13
+ * Crée une instance de Rating
14
+ * @param {CanvasFramework} framework - Framework parent
15
+ * @param {Object} [options={}] - Options de configuration
16
+ * @param {number} [options.max=5] - Nombre max d'étoiles
17
+ * @param {number} [options.value=0] - Valeur initiale
18
+ * @param {boolean} [options.allowHalf=false] - Autoriser demi-étoiles
19
+ * @param {boolean} [options.readOnly=false] - Mode lecture seule
20
+ * @param {number} [options.size=32] - Taille des étoiles
21
+ * @param {string} [options.activeColor] - Couleur étoiles actives
22
+ * @param {Function} [options.onChange] - Callback changement
23
+ */
24
+ constructor(framework, options = {}) {
25
+ super(framework, options);
26
+
27
+ this.platform = framework.platform;
28
+ this.max = options.max || 5;
29
+ this.value = options.value || 0;
30
+ this.allowHalf = options.allowHalf || false;
31
+ this.readOnly = options.readOnly || false;
32
+ this.size = options.size || 32;
33
+ this.onChange = options.onChange || (() => {});
34
+
35
+ // Couleurs selon plateforme
36
+ if (this.platform === 'material') {
37
+ this.activeColor = options.activeColor || '#FBC02D';
38
+ this.inactiveColor = '#E0E0E0';
39
+ this.rippleColor = 'rgba(251, 192, 45, 0.3)';
40
+ } else {
41
+ this.activeColor = options.activeColor || '#FFCC00';
42
+ this.inactiveColor = '#E5E5EA';
43
+ }
44
+
45
+ // Dimensions
46
+ const spacing = this.platform === 'material' ? 8 : 4;
47
+ this.width = this.max * (this.size + spacing);
48
+ this.height = this.size + 16;
49
+
50
+ // État hover/animation
51
+ this.hoveredStar = null;
52
+ this.pressedStar = null;
53
+ this.ripples = [];
54
+
55
+ // Positions des étoiles
56
+ this.starPositions = [];
57
+ this.calculateStarPositions();
58
+
59
+ // ✅ Flag anti-double appel
60
+ this._isChanging = false;
61
+ }
62
+
63
+ /**
64
+ * Calcule les positions des étoiles
65
+ * @private
66
+ */
67
+ calculateStarPositions() {
68
+ this.starPositions = [];
69
+ const spacing = this.platform === 'material' ? 8 : 4;
70
+
71
+ for (let i = 0; i < this.max; i++) {
72
+ const x = this.x + i * (this.size + spacing) + this.size / 2;
73
+ const y = this.y + this.height / 2;
74
+
75
+ this.starPositions.push({ x, y, index: i });
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Dessine une étoile pleine
81
+ * @private
82
+ */
83
+ drawStar(ctx, cx, cy, size, fillAmount = 1) {
84
+ const outerRadius = size / 2;
85
+ const innerRadius = outerRadius * 0.4;
86
+ const points = 5;
87
+
88
+ ctx.save();
89
+ ctx.translate(cx, cy);
90
+
91
+ // Créer le chemin de l'étoile
92
+ ctx.beginPath();
93
+ for (let i = 0; i < points * 2; i++) {
94
+ const radius = i % 2 === 0 ? outerRadius : innerRadius;
95
+ const angle = (i * Math.PI) / points - Math.PI / 2;
96
+ const x = Math.cos(angle) * radius;
97
+ const y = Math.sin(angle) * radius;
98
+
99
+ if (i === 0) {
100
+ ctx.moveTo(x, y);
101
+ } else {
102
+ ctx.lineTo(x, y);
103
+ }
104
+ }
105
+ ctx.closePath();
106
+
107
+ // Remplissage partiel si demi-étoile
108
+ if (fillAmount < 1 && fillAmount > 0) {
109
+ ctx.save();
110
+ ctx.clip();
111
+
112
+ // Partie inactive
113
+ ctx.fillStyle = this.inactiveColor;
114
+ ctx.fill();
115
+
116
+ // Partie active (gauche)
117
+ ctx.beginPath();
118
+ ctx.rect(-outerRadius, -outerRadius, outerRadius * 2 * fillAmount, outerRadius * 2);
119
+ ctx.clip();
120
+
121
+ // Redessiner l'étoile en couleur active
122
+ ctx.beginPath();
123
+ for (let i = 0; i < points * 2; i++) {
124
+ const radius = i % 2 === 0 ? outerRadius : innerRadius;
125
+ const angle = (i * Math.PI) / points - Math.PI / 2;
126
+ const x = Math.cos(angle) * radius;
127
+ const y = Math.sin(angle) * radius;
128
+
129
+ if (i === 0) {
130
+ ctx.moveTo(x, y);
131
+ } else {
132
+ ctx.lineTo(x, y);
133
+ }
134
+ }
135
+ ctx.closePath();
136
+ ctx.fillStyle = this.activeColor;
137
+ ctx.fill();
138
+
139
+ ctx.restore();
140
+ } else {
141
+ ctx.fillStyle = fillAmount > 0 ? this.activeColor : this.inactiveColor;
142
+ ctx.fill();
143
+ }
144
+
145
+ ctx.restore();
146
+ }
147
+
148
+ /**
149
+ * Dessine le rating Material
150
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
151
+ * @private
152
+ */
153
+ drawMaterial(ctx) {
154
+ this.starPositions.forEach((pos, index) => {
155
+ const starValue = index + 1;
156
+ const isHovered = this.hoveredStar === index;
157
+
158
+ // Ombre si hover (sauf read-only)
159
+ if (isHovered && !this.readOnly) {
160
+ ctx.save();
161
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
162
+ ctx.shadowBlur = 8;
163
+ ctx.shadowOffsetY = 2;
164
+ }
165
+
166
+ // Calculer le remplissage
167
+ let fillAmount = 0;
168
+ const hoverValue = this.hoveredStar !== null ?
169
+ (this.allowHalf ? this.getHoverValue() : this.hoveredStar + 1) :
170
+ this.value;
171
+
172
+ const compareValue = this.readOnly ? this.value : hoverValue;
173
+
174
+ if (compareValue >= starValue) {
175
+ fillAmount = 1;
176
+ } else if (this.allowHalf && compareValue >= starValue - 0.5) {
177
+ fillAmount = 0.5;
178
+ }
179
+
180
+ // Dessiner l'étoile
181
+ this.drawStar(ctx, pos.x, pos.y, this.size, fillAmount);
182
+
183
+ if (isHovered && !this.readOnly) {
184
+ ctx.restore();
185
+ }
186
+
187
+ // Ripple effect
188
+ if (this.platform === 'material') {
189
+ for (let ripple of this.ripples) {
190
+ if (ripple.star === index) {
191
+ ctx.save();
192
+ ctx.globalAlpha = ripple.opacity;
193
+ ctx.fillStyle = this.rippleColor;
194
+ ctx.beginPath();
195
+ ctx.arc(pos.x, pos.y, ripple.radius, 0, Math.PI * 2);
196
+ ctx.fill();
197
+ ctx.restore();
198
+ }
199
+ }
200
+ }
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Dessine le rating Cupertino (iOS)
206
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
207
+ * @private
208
+ */
209
+ drawCupertino(ctx) {
210
+ this.starPositions.forEach((pos, index) => {
211
+ const starValue = index + 1;
212
+ const isPressed = this.pressedStar === index;
213
+
214
+ // Scale animation si pressed
215
+ const scale = isPressed && !this.readOnly ? 1.2 : 1;
216
+
217
+ // Calculer le remplissage
218
+ let fillAmount = 0;
219
+ const hoverValue = this.hoveredStar !== null ?
220
+ (this.allowHalf ? this.getHoverValue() : this.hoveredStar + 1) :
221
+ this.value;
222
+
223
+ const compareValue = this.readOnly ? this.value : hoverValue;
224
+
225
+ if (compareValue >= starValue) {
226
+ fillAmount = 1;
227
+ } else if (this.allowHalf && compareValue >= starValue - 0.5) {
228
+ fillAmount = 0.5;
229
+ }
230
+
231
+ // Appliquer scale
232
+ ctx.save();
233
+ ctx.translate(pos.x, pos.y);
234
+ ctx.scale(scale, scale);
235
+ ctx.translate(-pos.x, -pos.y);
236
+
237
+ // Dessiner l'étoile
238
+ this.drawStar(ctx, pos.x, pos.y, this.size, fillAmount);
239
+
240
+ ctx.restore();
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Dessine le composant
246
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
247
+ */
248
+ draw(ctx) {
249
+ ctx.save();
250
+
251
+ if (this.platform === 'material') {
252
+ this.drawMaterial(ctx);
253
+ } else {
254
+ this.drawCupertino(ctx);
255
+ }
256
+
257
+ ctx.restore();
258
+ }
259
+
260
+ /**
261
+ * Gère le clic/touch
262
+ * @param {number} x - Coordonnée X
263
+ * @param {number} y - Coordonnée Y
264
+ */
265
+ handleClick(x, y) {
266
+ if (this.readOnly) return false;
267
+
268
+ const starIndex = this.getStarAtPoint(x, y);
269
+ if (starIndex !== null) {
270
+ this.pressedStar = starIndex;
271
+
272
+ // Calculer la nouvelle valeur
273
+ let newValue = starIndex + 1;
274
+
275
+ if (this.allowHalf) {
276
+ const star = this.starPositions[starIndex];
277
+ const relativeX = x - (star.x - this.size / 2);
278
+ newValue = relativeX < this.size / 2 ? starIndex + 0.5 : starIndex + 1;
279
+ }
280
+
281
+ // Ripple Material
282
+ if (this.platform === 'material') {
283
+ const star = this.starPositions[starIndex];
284
+ this.ripples.push({
285
+ star: starIndex,
286
+ x: star.x,
287
+ y: star.y,
288
+ radius: 0,
289
+ maxRadius: this.size,
290
+ opacity: 1
291
+ });
292
+ this.animateRipple();
293
+ }
294
+
295
+ // Mettre à jour
296
+ this.value = newValue;
297
+ this.onChange(this.value);
298
+ this.framework.redraw();
299
+
300
+ // Reset pressed après animation
301
+ setTimeout(() => {
302
+ this.pressedStar = null;
303
+ this.framework.redraw();
304
+ }, this.platform === 'material' ? 100 : 150);
305
+
306
+ return true;
307
+ }
308
+
309
+ return false;
310
+ }
311
+
312
+ /**
313
+ * Gère le survol
314
+ * @param {number} x - Coordonnée X
315
+ * @param {number} y - Coordonnée Y
316
+ */
317
+ handleHover(x, y) {
318
+ if (this.readOnly) return;
319
+
320
+ const starIndex = this.getStarAtPoint(x, y);
321
+
322
+ if (starIndex !== this.hoveredStar) {
323
+ this.hoveredStar = starIndex;
324
+ this.framework.redraw();
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Gère la sortie du survol
330
+ */
331
+ handleHoverEnd() {
332
+ if (this.hoveredStar !== null) {
333
+ this.hoveredStar = null;
334
+ this.framework.redraw();
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Obtient l'index de l'étoile à une position
340
+ * @private
341
+ */
342
+ getStarAtPoint(x, y) {
343
+ for (let i = 0; i < this.starPositions.length; i++) {
344
+ const star = this.starPositions[i];
345
+ const dx = x - star.x;
346
+ const dy = y - star.y;
347
+ const distance = Math.sqrt(dx * dx + dy * dy);
348
+
349
+ if (distance <= this.size / 2) {
350
+ return i;
351
+ }
352
+ }
353
+ return null;
354
+ }
355
+
356
+ /**
357
+ * Obtient la valeur hover (avec demi-étoiles)
358
+ * @private
359
+ */
360
+ getHoverValue() {
361
+ if (this.hoveredStar === null) return this.value;
362
+
363
+ // Pour l'instant retourner valeur entière
364
+ // (nécessite position X précise pour demi-étoiles)
365
+ return this.hoveredStar + 1;
366
+ }
367
+
368
+ /**
369
+ * Anime les ripples Material
370
+ * @private
371
+ */
372
+ animateRipple() {
373
+ const animate = () => {
374
+ for (let ripple of this.ripples) {
375
+ ripple.radius += ripple.maxRadius / 10;
376
+ ripple.opacity -= 0.08;
377
+ }
378
+
379
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
380
+
381
+ if (this.framework && this.framework.redraw) {
382
+ this.framework.redraw();
383
+ }
384
+
385
+ if (this.ripples.length > 0) {
386
+ requestAnimationFrame(animate);
387
+ }
388
+ };
389
+
390
+ animate();
391
+ }
392
+
393
+ /**
394
+ * Définit la valeur
395
+ * @param {number} value - Nouvelle valeur
396
+ */
397
+ setValue(value) {
398
+ this.value = Math.max(0, Math.min(this.max, value));
399
+ this.framework.redraw();
400
+ }
401
+
402
+ /**
403
+ * Obtient la valeur
404
+ * @returns {number}
405
+ */
406
+ getValue() {
407
+ return this.value;
408
+ }
409
+
410
+ /**
411
+ * Vérifie si un point est dans les limites
412
+ */
413
+ isPointInside(x, y) {
414
+ // Vérifier si le clic est dans le composant
415
+ if (x < this.x || x > this.x + this.width ||
416
+ y < this.y || y > this.y + this.height) {
417
+ return false;
418
+ }
419
+
420
+ // Mode lecture seule - on ne fait rien
421
+ if (this.readOnly) {
422
+ return true; // Le clic est dans le composant mais on ignore
423
+ }
424
+
425
+ // Vérifier si on clique sur une étoile
426
+ const starIndex = this.getStarAtPoint(x, y);
427
+
428
+ // Gestion du survol (même sans clic)
429
+ if (starIndex !== this.hoveredStar) {
430
+ this.hoveredStar = starIndex;
431
+ if (this.framework && this.framework.redraw) {
432
+ this.framework.redraw();
433
+ }
434
+ }
435
+
436
+ // Si on clique sur une étoile
437
+ if (starIndex !== null) {
438
+ // ✅ Éviter les appels multiples
439
+ if (this._isChanging) return true;
440
+ this._isChanging = true;
441
+
442
+ this.pressedStar = starIndex;
443
+
444
+ // Calculer la nouvelle valeur
445
+ let newValue = starIndex + 1;
446
+
447
+ if (this.allowHalf) {
448
+ const star = this.starPositions[starIndex];
449
+ const relativeX = x - (star.x - this.size / 2);
450
+ newValue = relativeX < this.size / 2 ? starIndex + 0.5 : starIndex + 1;
451
+ }
452
+
453
+ // Ripple Material
454
+ if (this.platform === 'material') {
455
+ const star = this.starPositions[starIndex];
456
+ this.ripples.push({
457
+ star: starIndex,
458
+ x: star.x,
459
+ y: star.y,
460
+ radius: 0,
461
+ maxRadius: this.size,
462
+ opacity: 1
463
+ });
464
+ this.animateRipple();
465
+ }
466
+
467
+ // Mettre à jour la valeur
468
+ this.value = newValue;
469
+ this.onChange(this.value);
470
+
471
+ // Redessiner immédiatement
472
+ if (this.framework && this.framework.redraw) {
473
+ this.framework.redraw();
474
+ }
475
+
476
+ // Reset pressed après animation
477
+ setTimeout(() => {
478
+ this.pressedStar = null;
479
+ // ✅ Réinitialiser le flag
480
+ this._isChanging = false;
481
+ if (this.framework && this.framework.redraw) {
482
+ this.framework.redraw();
483
+ }
484
+ }, this.platform === 'material' ? 100 : 150);
485
+
486
+ return true;
487
+ }
488
+
489
+ // Clic dans le composant mais pas sur une étoile
490
+ return true;
491
+ }
492
+ }
493
+
494
+ export default Rating;
@@ -60,6 +60,11 @@ import FloatedCamera from '../components/FloatedCamera.js';
60
60
  import TimePicker from '../components/TimePicker.js';
61
61
  import QRCodeReader from '../components/QRCodeReader.js';
62
62
  import QRCodeGenerator from '../components/QRCodeGenerator.js';
63
+ import PaginatedContainer from '../components/PaginatedContainer.js';
64
+ import ColorPicker from '../components/ColorPicker.js';
65
+ import Rating from '../components/Rating.js';
66
+ import Breadcrumb from '../components/Breadcrumb.js';
67
+ import Popover from '../components/Popover.js';
63
68
 
64
69
  // Utils
65
70
  import SafeArea from '../utils/SafeArea.js';
package/core/UIBuilder.js CHANGED
@@ -60,6 +60,11 @@ import FloatedCamera from '../components/FloatedCamera.js';
60
60
  import TimePicker from '../components/TimePicker.js';
61
61
  import QRCodeReader from '../components/QRCodeReader.js';
62
62
  import QRCodeGenerator from '../components/QRCodeGenerator.js';
63
+ import PaginatedContainer from '../components/PaginatedContainer.js';
64
+ import ColorPicker from '../components/ColorPicker.js';
65
+ import Rating from '../components/Rating.js';
66
+ import Breadcrumb from '../components/Breadcrumb.js';
67
+ import Popover from '../components/Popover.js';
63
68
 
64
69
  // Features
65
70
  import PullToRefresh from '../features/PullToRefresh.js';
@@ -145,7 +150,12 @@ const Components = {
145
150
  Column,
146
151
  Positioned,
147
152
  Banner,
153
+ PaginatedContainer,
154
+ ColorPicker,
148
155
  Chart,
156
+ Rating,
157
+ Breadcrumb,
158
+ Popover,
149
159
  QRCodeGenerator,
150
160
  Stack
151
161
  };
package/index.js CHANGED
@@ -67,6 +67,11 @@ export { default as FloatedCamera } from './components/FloatedCamera.js';
67
67
  export { default as TimePicker } from './components/TimePicker.js';
68
68
  export { default as QRCodeReader } from './components/QRCodeReader.js';
69
69
  export { default as QRCodeGenerator } from './components/QRCodeGenerator.js';
70
+ export { default as PaginatedContainer } from './components/PaginatedContainer.js';
71
+ export { default as ColorPicker } from './components/ColorPicker.js';
72
+ export { default as Rating } from './components/Rating.js';
73
+ export { default as Breadcrumb } from './components/Breadcrumb.js';
74
+ export { default as Popover } from './components/Popover.js';
70
75
 
71
76
  // Utils
72
77
  export { default as SafeArea } from './utils/SafeArea.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.5.58",
3
+ "version": "0.5.60",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"