canvasframework 0.5.41 → 0.5.42

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.
@@ -1,315 +1,292 @@
1
1
  import Component from '../core/Component.js';
2
2
 
3
3
  /**
4
- * Zone de téléchargement de fichiers avec drag & drop
4
+ * Zone de téléchargement de fichiers (Material & Cupertino) - Bouton visuel seulement
5
5
  * @class
6
6
  * @extends Component
7
- * @property {string} label - Texte affiché
8
- * @property {string} sublabel - Sous-texte
9
- * @property {string} accept - Types de fichiers acceptés
10
- * @property {boolean} multiple - Accepter plusieurs fichiers
11
- * @property {number} maxSize - Taille max en bytes
12
- * @property {Array} files - Fichiers sélectionnés
13
- * @property {boolean} isDragOver - État de survol
14
- * @property {string} borderColor - Couleur de bordure
15
- * @property {string} bgColor - Couleur de fond
16
- * @property {string} iconColor - Couleur de l'icône
17
- * @property {Function} onFilesSelected - Callback
18
- * @property {Function} onError - Callback d'erreur
19
7
  */
20
8
  class FileUpload extends Component {
21
- /**
22
- * Crée une instance de FileUpload
23
- * @param {CanvasFramework} framework - Framework parent
24
- * @param {Object} [options={}] - Options de configuration
25
- * @param {string} [options.label='Drag & drop files here'] - Label
26
- * @param {string} [options.sublabel='or click to browse'] - Sublabel
27
- * @param {string} [options.accept='*'] - Types acceptés
28
- * @param {boolean} [options.multiple=true] - Multiple fichiers
29
- * @param {number} [options.maxSize=10485760] - Taille max (10MB)
30
- * @param {Function} [options.onFilesSelected] - Callback
31
- * @param {Function} [options.onError] - Callback erreur
32
- */
33
9
  constructor(framework, options = {}) {
34
10
  super(framework, options);
35
-
36
- this.label = options.label || 'Drag & drop files here';
37
- this.sublabel = options.sublabel || 'or click to browse';
11
+
12
+ this.label = options.label || 'Cliquez pour choisir un fichier';
38
13
  this.accept = options.accept || '*';
39
14
  this.multiple = options.multiple !== false;
40
- this.maxSize = options.maxSize || 10485760; // 10MB
41
- this.files = [];
42
- this.isDragOver = false;
43
-
44
- const platform = framework.platform;
45
-
15
+ this.files = options.files || [];
16
+ this.platform = framework.platform;
17
+
46
18
  // Styles selon la plateforme
47
- if (platform === 'material') {
48
- this.borderColor = '#6200EE';
49
- this.bgColor = 'rgba(98, 0, 238, 0.05)';
50
- this.iconColor = '#6200EE';
51
- this.borderRadius = 4;
52
- this.borderWidth = 2;
19
+ if (this.platform === 'material') {
20
+ // DESIGN MATERIAL AVEC RIPPLE PRONONCÉ
21
+ this.bgColor = options.bgColor || 'rgba(98, 0, 238, 0.04)';
22
+ this.borderColor = options.borderColor || '#6200EE';
23
+ this.iconColor = options.iconColor || '#6200EE';
24
+ this.borderRadius = options.borderRadius || 8;
25
+ this.borderWidth = options.borderWidth || 1.5;
26
+ this.borderStyle = 'dashed';
27
+ this.height = options.height || 90;
28
+ this.rippleColor = 'rgba(98, 0, 238, 0.3)'; // Ripple plus visible
29
+ this.elevation = 1;
53
30
  } else {
54
- this.borderColor = '#007AFF';
55
- this.bgColor = 'rgba(0, 122, 255, 0.05)';
56
- this.iconColor = '#007AFF';
57
- this.borderRadius = 12;
58
- this.borderWidth = 2;
31
+ // DESIGN CUPERTINO
32
+ this.bgColor = options.bgColor || '#F2F2F7';
33
+ this.borderColor = options.borderColor || '#C6C6C8';
34
+ this.iconColor = options.iconColor || '#007AFF';
35
+ this.borderRadius = 14;
36
+ this.borderWidth = 0;
37
+ this.height = options.height || 80;
38
+ this.borderStyle = 'solid';
59
39
  }
60
40
 
41
+ this.width = options.width || 300;
42
+
43
+ // Ripple effect Material - variables d'animation
44
+ this.ripples = [];
45
+
46
+ this.onClickCallback = options.onClick || null;
61
47
  this.onFilesSelected = options.onFilesSelected || null;
62
- this.onError = options.onError || null;
48
+
49
+ if (options.onFilesSelected && !options.onClickCallback) {
50
+ this.onClickCallback = options.onFilesSelected;
51
+ }
63
52
 
64
- // Créer un input file caché
65
- this.createFileInput();
53
+ // Bind
54
+ this.onPress = this.handlePress.bind(this);
66
55
  }
67
56
 
68
57
  /**
69
- * Crée l'input file HTML caché
70
- * @private
58
+ * Gère la pression sur le bouton
71
59
  */
72
- createFileInput() {
73
- this.fileInput = document.createElement('input');
74
- this.fileInput.type = 'file';
75
- this.fileInput.accept = this.accept;
76
- this.fileInput.multiple = this.multiple;
77
- this.fileInput.style.display = 'none';
78
- document.body.appendChild(this.fileInput);
79
-
80
- this.fileInput.addEventListener('change', (e) => {
81
- this.handleFiles(Array.from(e.target.files));
82
- });
60
+ handlePress(x, y) {
61
+ if (this.platform === 'material') {
62
+ const adjustedY = y - (this.framework.scrollOffset || 0);
63
+ this.ripples.push({
64
+ x: x - this.x,
65
+ y: adjustedY - this.y,
66
+ radius: 0,
67
+ maxRadius: Math.max(this.width, this.height) * 1.5,
68
+ opacity: 1
69
+ });
70
+ this.animateRipple();
71
+ }
83
72
  }
84
73
 
85
74
  /**
86
- * Gère les fichiers sélectionnés
87
- * @param {Array} fileList - Liste des fichiers
88
- * @private
75
+ * Anime les effets ripple
89
76
  */
90
- handleFiles(fileList) {
91
- const validFiles = [];
92
-
93
- for (let file of fileList) {
94
- // Vérifier la taille
95
- if (file.size > this.maxSize) {
96
- if (this.onError) {
97
- this.onError({
98
- type: 'size',
99
- message: `${file.name} exceeds max size of ${this.formatBytes(this.maxSize)}`,
100
- file: file
101
- });
102
- }
103
- continue;
77
+ animateRipple() {
78
+ const animate = () => {
79
+ for (let ripple of this.ripples) {
80
+ ripple.radius += ripple.maxRadius / 15;
81
+ ripple.opacity -= 0.05;
104
82
  }
105
-
106
- // Vérifier le type si spécifié
107
- if (this.accept !== '*') {
108
- const acceptedTypes = this.accept.split(',').map(t => t.trim());
109
- const fileType = file.type;
110
- const fileExt = '.' + file.name.split('.').pop();
111
-
112
- const isAccepted = acceptedTypes.some(type => {
113
- if (type.startsWith('.')) {
114
- return fileExt === type;
115
- } else if (type.endsWith('/*')) {
116
- return fileType.startsWith(type.replace('/*', ''));
117
- } else {
118
- return fileType === type;
119
- }
120
- });
121
-
122
- if (!isAccepted) {
123
- if (this.onError) {
124
- this.onError({
125
- type: 'type',
126
- message: `${file.name} is not an accepted file type`,
127
- file: file
128
- });
129
- }
130
- continue;
131
- }
83
+
84
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
85
+
86
+ if (this.framework && this.framework.redraw) {
87
+ this.framework.redraw();
132
88
  }
133
-
134
- validFiles.push(file);
135
- }
136
-
137
- if (validFiles.length > 0) {
138
- this.files = validFiles;
139
- if (this.onFilesSelected) {
140
- this.onFilesSelected(validFiles);
89
+
90
+ if (this.ripples.length > 0) {
91
+ requestAnimationFrame(animate);
141
92
  }
93
+ };
94
+
95
+ animate();
96
+ }
97
+
98
+ handleRelease() {
99
+ this.pressed = false;
100
+
101
+ if (this.onClickCallback) {
102
+ this.onClickCallback(this.files);
142
103
  }
143
104
 
144
- // Reset input
145
- this.fileInput.value = '';
105
+ if (this.framework && this.framework.redraw) {
106
+ this.framework.redraw();
107
+ }
146
108
  }
147
109
 
148
- /**
149
- * Formate les bytes en format lisible
150
- * @param {number} bytes - Nombre de bytes
151
- * @returns {string} Taille formatée
152
- * @private
153
- */
154
- formatBytes(bytes) {
155
- if (bytes === 0) return '0 Bytes';
156
- const k = 1024;
157
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
158
- const i = Math.floor(Math.log(bytes) / Math.log(k));
159
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
110
+ setFiles(files) {
111
+ this.files = files;
112
+ if (this.framework && this.framework.redraw) {
113
+ this.framework.redraw();
114
+ }
115
+ }
116
+
117
+ addFile(file) {
118
+ this.files.push(file);
119
+ if (this.framework && this.framework.redraw) {
120
+ this.framework.redraw();
121
+ }
122
+ }
123
+
124
+ clearFiles() {
125
+ this.files = [];
126
+ if (this.framework && this.framework.redraw) {
127
+ this.framework.redraw();
128
+ }
160
129
  }
161
130
 
162
- /**
163
- * Dessine le composant
164
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
165
- */
166
131
  draw(ctx) {
167
132
  ctx.save();
133
+
134
+ const radius = this.borderRadius;
168
135
 
169
- // Fond
170
- ctx.fillStyle = this.isDragOver || this.pressed ?
171
- this.lightenColor(this.bgColor) : this.bgColor;
136
+ // OMBRE POUR MATERIAL
137
+ if (this.platform === 'material' && this.elevation > 0 && !this.pressed) {
138
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.08)';
139
+ ctx.shadowBlur = this.elevation * 4;
140
+ ctx.shadowOffsetY = this.elevation;
141
+ }
142
+
143
+ // Background
144
+ let currentBgColor = this.bgColor;
145
+ if (this.pressed && this.platform === 'cupertino') {
146
+ currentBgColor = '#E5E5EA';
147
+ }
148
+
149
+ ctx.fillStyle = currentBgColor;
172
150
  ctx.beginPath();
173
- this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
151
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, radius);
174
152
  ctx.fill();
175
-
176
- // Bordure en pointillés
177
- ctx.strokeStyle = this.borderColor;
178
- ctx.lineWidth = this.borderWidth;
179
- ctx.setLineDash([8, 8]);
180
- ctx.beginPath();
181
- this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
182
- ctx.stroke();
183
- ctx.setLineDash([]);
184
-
185
- // Icône de fichier (simple)
186
- const iconSize = 40;
187
- const iconX = this.x + this.width / 2 - iconSize / 2;
188
- const iconY = this.y + this.height / 2 - 40;
189
-
153
+
154
+ // Reset shadow
155
+ ctx.shadowColor = 'transparent';
156
+ ctx.shadowBlur = 0;
157
+ ctx.shadowOffsetY = 0;
158
+
159
+ // BORDURE
160
+ if (this.borderWidth > 0) {
161
+ ctx.strokeStyle = this.borderColor;
162
+ ctx.lineWidth = this.borderWidth;
163
+
164
+ if (this.platform === 'material' && this.borderStyle === 'dashed') {
165
+ ctx.setLineDash([6, 4]);
166
+ } else {
167
+ ctx.setLineDash([]);
168
+ }
169
+
170
+ ctx.beginPath();
171
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, radius);
172
+ ctx.stroke();
173
+ ctx.setLineDash([]);
174
+ }
175
+
176
+ // RIPPLE EFFECT (Material)
177
+ if (this.platform === 'material') {
178
+ ctx.save();
179
+ ctx.beginPath();
180
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, radius);
181
+ ctx.clip();
182
+
183
+ for (let ripple of this.ripples) {
184
+ ctx.globalAlpha = ripple.opacity;
185
+ ctx.fillStyle = this.rippleColor;
186
+ ctx.beginPath();
187
+ ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
188
+ ctx.fill();
189
+ }
190
+
191
+ ctx.restore();
192
+ }
193
+
194
+ // ICÔNE
195
+ const iconSize = this.platform === 'material' ? 32 : 28;
196
+ const iconX = this.x + this.width / 2;
197
+ const iconY = this.y + this.height / 2 - (this.files.length > 0 ? 15 : 8);
198
+
190
199
  ctx.strokeStyle = this.iconColor;
191
- ctx.lineWidth = 3;
192
-
193
- // Document
194
- ctx.beginPath();
195
- ctx.moveTo(iconX, iconY);
196
- ctx.lineTo(iconX + iconSize * 0.7, iconY);
197
- ctx.lineTo(iconX + iconSize, iconY + iconSize * 0.3);
198
- ctx.lineTo(iconX + iconSize, iconY + iconSize);
199
- ctx.lineTo(iconX, iconY + iconSize);
200
- ctx.closePath();
201
- ctx.stroke();
202
-
203
- // Coin plié
204
- ctx.beginPath();
205
- ctx.moveTo(iconX + iconSize * 0.7, iconY);
206
- ctx.lineTo(iconX + iconSize * 0.7, iconY + iconSize * 0.3);
207
- ctx.lineTo(iconX + iconSize, iconY + iconSize * 0.3);
208
- ctx.stroke();
209
-
210
- // Flèche montante
211
- const arrowX = iconX + iconSize / 2;
212
- const arrowY = iconY + iconSize * 0.5;
213
- const arrowSize = 12;
200
+ ctx.lineWidth = this.platform === 'material' ? 1.8 : 2;
201
+ ctx.lineCap = 'round';
214
202
 
203
+ // Ligne horizontale
215
204
  ctx.beginPath();
216
- ctx.moveTo(arrowX, arrowY - arrowSize);
217
- ctx.lineTo(arrowX, arrowY + arrowSize);
205
+ ctx.moveTo(iconX - iconSize / 3, iconY);
206
+ ctx.lineTo(iconX + iconSize / 3, iconY);
218
207
  ctx.stroke();
219
208
 
209
+ // Ligne verticale
220
210
  ctx.beginPath();
221
- ctx.moveTo(arrowX - arrowSize / 2, arrowY - arrowSize / 2);
222
- ctx.lineTo(arrowX, arrowY - arrowSize);
223
- ctx.lineTo(arrowX + arrowSize / 2, arrowY - arrowSize / 2);
211
+ ctx.moveTo(iconX, iconY - iconSize / 3);
212
+ ctx.lineTo(iconX, iconY + iconSize / 3);
224
213
  ctx.stroke();
225
-
226
- // Texte
227
- ctx.fillStyle = '#000000';
228
- ctx.font = '16px -apple-system, BlinkMacSystemFont, Roboto, sans-serif';
214
+
215
+ // LABEL
216
+ ctx.fillStyle = this.platform === 'material' ? '#5F6368' : '#3C3C43';
217
+ ctx.font = this.platform === 'material'
218
+ ? '500 13px Roboto, sans-serif'
219
+ : '15px -apple-system, sans-serif';
229
220
  ctx.textAlign = 'center';
230
221
  ctx.textBaseline = 'middle';
231
- ctx.fillText(this.label, this.x + this.width / 2, this.y + this.height / 2 + 30);
232
-
233
- ctx.fillStyle = '#666666';
234
- ctx.font = '14px -apple-system, BlinkMacSystemFont, Roboto, sans-serif';
235
- ctx.fillText(this.sublabel, this.x + this.width / 2, this.y + this.height / 2 + 52);
236
222
 
237
- // Afficher les fichiers sélectionnés
223
+ const labelY = iconY + iconSize / 2 + 12;
224
+ ctx.fillText(this.label, this.x + this.width / 2, labelY);
225
+
226
+ // FICHIERS SÉLECTIONNÉS
238
227
  if (this.files.length > 0) {
239
- ctx.fillStyle = this.borderColor;
240
- ctx.font = '12px -apple-system, BlinkMacSystemFont, Roboto, sans-serif';
241
- const fileText = this.files.length === 1 ?
242
- this.files[0].name :
243
- `${this.files.length} files selected`;
244
- ctx.fillText(fileText, this.x + this.width / 2, this.y + this.height - 20);
228
+ ctx.fillStyle = this.platform === 'material' ? '#6200EE' : '#007AFF';
229
+ ctx.font = this.platform === 'material'
230
+ ? '400 11px Roboto, sans-serif'
231
+ : '13px -apple-system, sans-serif';
232
+
233
+ let fileText = '';
234
+ if (this.files.length === 1) {
235
+ fileText = this.truncateText(this.files[0].name, 25);
236
+ } else {
237
+ fileText = `${this.files.length} fichier${this.files.length > 1 ? 's' : ''}`;
238
+ }
239
+
240
+ const fileIconY = this.y + this.height - 18;
241
+
242
+ if (this.platform === 'material') {
243
+ ctx.fillStyle = '#6200EE';
244
+ ctx.beginPath();
245
+ ctx.roundRect(this.x + this.width / 2 - 40, fileIconY - 2, 8, 10, 1);
246
+ ctx.fill();
247
+ ctx.beginPath();
248
+ ctx.moveTo(this.x + this.width / 2 - 32, fileIconY - 2);
249
+ ctx.lineTo(this.x + this.width / 2 - 32, fileIconY + 8);
250
+ ctx.lineTo(this.x + this.width / 2 - 40, fileIconY + 8);
251
+ ctx.closePath();
252
+ ctx.fill();
253
+
254
+ ctx.fillText(fileText, this.x + this.width / 2 + 5, fileIconY + 3);
255
+ } else {
256
+ ctx.fillText(`📎 ${fileText}`, this.x + this.width / 2, fileIconY);
257
+ }
245
258
  }
246
-
259
+
247
260
  ctx.restore();
248
261
  }
249
262
 
250
- /**
251
- * Dessine un rectangle avec coins arrondis
252
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
253
- * @param {number} x - Position X
254
- * @param {number} y - Position Y
255
- * @param {number} width - Largeur
256
- * @param {number} height - Hauteur
257
- * @param {number} radius - Rayon des coins
258
- * @private
259
- */
260
- roundRect(ctx, x, y, width, height, radius) {
261
- ctx.moveTo(x + radius, y);
262
- ctx.lineTo(x + width - radius, y);
263
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
264
- ctx.lineTo(x + width, y + height - radius);
265
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
266
- ctx.lineTo(x + radius, y + height);
267
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
268
- ctx.lineTo(x, y + radius);
269
- ctx.quadraticCurveTo(x, y, x + radius, y);
263
+ roundRect(ctx, x, y, w, h, r) {
264
+ if (r > 0) {
265
+ ctx.moveTo(x + r, y);
266
+ ctx.lineTo(x + w - r, y);
267
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
268
+ ctx.lineTo(x + w, y + h - r);
269
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
270
+ ctx.lineTo(x + r, y + h);
271
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
272
+ ctx.lineTo(x, y + r);
273
+ ctx.quadraticCurveTo(x, y, x + r, y);
274
+ } else {
275
+ ctx.rect(x, y, w, h);
276
+ }
270
277
  }
271
278
 
272
- /**
273
- * Éclaircit une couleur
274
- * @param {string} color - Couleur
275
- * @returns {string} Couleur éclaircie
276
- * @private
277
- */
278
- lightenColor(color) {
279
- if (color.startsWith('rgba')) {
280
- return color.replace(/[\d.]+\)$/g, '0.15)');
281
- }
282
- return color;
279
+ truncateText(text, maxLength) {
280
+ if (text.length <= maxLength) return text;
281
+ return text.substring(0, maxLength - 3) + '...';
283
282
  }
284
283
 
285
- /**
286
- * Vérifie si un point est dans les limites
287
- * @param {number} x - Coordonnée X
288
- * @param {number} y - Coordonnée Y
289
- * @returns {boolean} True si le point est dans le composant
290
- */
291
284
  isPointInside(x, y) {
292
285
  return x >= this.x &&
293
286
  x <= this.x + this.width &&
294
287
  y >= this.y &&
295
288
  y <= this.y + this.height;
296
289
  }
297
-
298
- /**
299
- * Override du onClick pour ouvrir le file picker
300
- */
301
- onClick() {
302
- this.fileInput.click();
303
- }
304
-
305
- /**
306
- * Nettoie le composant
307
- */
308
- destroy() {
309
- if (this.fileInput && this.fileInput.parentNode) {
310
- this.fileInput.parentNode.removeChild(this.fileInput);
311
- }
312
- }
313
290
  }
314
291
 
315
292
  export default FileUpload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.5.41",
3
+ "version": "0.5.42",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"