canvasframework 0.3.8 → 0.3.10

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,443 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Sélecteur d'heure (Material horloge & iOS wheel)
5
+ * @class
6
+ * @extends Component
7
+ */
8
+ class TimePicker extends Component {
9
+ /**
10
+ * Crée une instance de TimePicker
11
+ * @param {CanvasFramework} framework - Framework parent
12
+ * @param {Object} [options={}] - Options
13
+ * @param {number} [options.hours=12] - Heures (0-23)
14
+ * @param {number} [options.minutes=0] - Minutes (0-59)
15
+ * @param {boolean} [options.is24Hour=true] - Format 24h
16
+ * @param {Function} [options.onChange] - Callback (hours, minutes)
17
+ */
18
+ constructor(framework, options = {}) {
19
+ super(framework, options);
20
+
21
+ this.hours = options.hours || 12;
22
+ this.minutes = options.minutes || 0;
23
+ this.is24Hour = options.is24Hour !== false;
24
+ this.onChange = options.onChange;
25
+ this.platform = framework.platform;
26
+
27
+ this.mode = 'hours'; // 'hours' ou 'minutes'
28
+ this.isOpen = false;
29
+
30
+ // Dimensions
31
+ if (this.platform === 'cupertino') {
32
+ // iOS wheel picker
33
+ this.pickerHeight = 216;
34
+ this.wheelRadius = 40;
35
+ } else {
36
+ // Material clock
37
+ this.clockRadius = 100;
38
+ this.pickerHeight = 280;
39
+ }
40
+
41
+ this.width = options.width || framework.width - 40;
42
+ this.height = 56;
43
+ }
44
+
45
+ /**
46
+ * Ouvre/ferme le picker
47
+ */
48
+ togglePicker() {
49
+ this.isOpen = !this.isOpen;
50
+ }
51
+
52
+ /**
53
+ * Gère le clic
54
+ */
55
+ onClick() {
56
+ const framework = this.framework;
57
+ const clickInfo = framework.getLastClick();
58
+
59
+ if (clickInfo) {
60
+ const { x, y } = clickInfo;
61
+
62
+ if (!this.isOpen) {
63
+ this.togglePicker();
64
+ return;
65
+ }
66
+
67
+ if (this.platform === 'material') {
68
+ this.handleMaterialClick(x, y);
69
+ } else {
70
+ this.handleIOSClick(x, y);
71
+ }
72
+ } else {
73
+ this.togglePicker();
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Gère le clic Material (horloge)
79
+ * @private
80
+ */
81
+ handleMaterialClick(x, y) {
82
+ const pickerY = this.y + this.height + 8;
83
+ const centerX = this.x + this.width / 2;
84
+ const centerY = pickerY + 140;
85
+
86
+ // Toggle mode heures/minutes
87
+ if (y >= pickerY + 20 && y <= pickerY + 60) {
88
+ if (x < this.x + this.width / 2) {
89
+ this.mode = 'hours';
90
+ } else {
91
+ this.mode = 'minutes';
92
+ }
93
+ return;
94
+ }
95
+
96
+ // Clic sur l'horloge
97
+ const dx = x - centerX;
98
+ const dy = y - centerY;
99
+ const distance = Math.sqrt(dx * dx + dy * dy);
100
+
101
+ if (distance <= this.clockRadius) {
102
+ const angle = Math.atan2(dy, dx);
103
+ const normalizedAngle = (angle + Math.PI / 2 + 2 * Math.PI) % (2 * Math.PI);
104
+
105
+ if (this.mode === 'hours') {
106
+ const hourValue = Math.round((normalizedAngle / (2 * Math.PI)) * 12);
107
+ this.hours = hourValue === 0 ? 12 : hourValue;
108
+ if (!this.is24Hour && this.hours > 12) this.hours -= 12;
109
+ } else {
110
+ this.minutes = Math.round((normalizedAngle / (2 * Math.PI)) * 60) % 60;
111
+ }
112
+
113
+ if (this.onChange) {
114
+ this.onChange(this.hours, this.minutes);
115
+ }
116
+ }
117
+
118
+ // Bouton OK
119
+ const okButtonY = pickerY + this.pickerHeight - 50;
120
+ if (y >= okButtonY && y <= okButtonY + 40 &&
121
+ x >= this.x + this.width - 100 && x <= this.x + this.width - 20) {
122
+ this.isOpen = false;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Gère le clic iOS (wheel)
128
+ * @private
129
+ */
130
+ handleIOSClick(x, y) {
131
+ // Bouton Done
132
+ const pickerY = this.y + this.height + 8;
133
+ const doneY = pickerY + this.pickerHeight - 44;
134
+
135
+ if (y >= doneY && y <= doneY + 44) {
136
+ this.isOpen = false;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Dessine le composant
142
+ */
143
+ draw(ctx) {
144
+ ctx.save();
145
+
146
+ // Bouton pour ouvrir le picker
147
+ this.drawButton(ctx);
148
+
149
+ // Picker
150
+ if (this.isOpen) {
151
+ if (this.platform === 'material') {
152
+ this.drawMaterialPicker(ctx);
153
+ } else {
154
+ this.drawIOSPicker(ctx);
155
+ }
156
+ }
157
+
158
+ ctx.restore();
159
+ }
160
+
161
+ /**
162
+ * Dessine le bouton
163
+ * @private
164
+ */
165
+ drawButton(ctx) {
166
+ if (this.platform === 'material') {
167
+ // Material button
168
+ ctx.fillStyle = '#FFFFFF';
169
+ ctx.fillRect(this.x, this.y, this.width, this.height);
170
+ ctx.strokeStyle = '#E0E0E0';
171
+ ctx.lineWidth = 1;
172
+ ctx.strokeRect(this.x, this.y, this.width, this.height);
173
+
174
+ // Icône horloge
175
+ ctx.strokeStyle = '#666666';
176
+ ctx.lineWidth = 2;
177
+ ctx.beginPath();
178
+ ctx.arc(this.x + 25, this.y + 28, 10, 0, Math.PI * 2);
179
+ ctx.stroke();
180
+ ctx.beginPath();
181
+ ctx.moveTo(this.x + 25, this.y + 28);
182
+ ctx.lineTo(this.x + 25, this.y + 22);
183
+ ctx.lineTo(this.x + 30, this.y + 28);
184
+ ctx.stroke();
185
+
186
+ // Texte
187
+ ctx.fillStyle = '#000000';
188
+ ctx.font = '16px Roboto, sans-serif';
189
+ ctx.textAlign = 'left';
190
+ ctx.textBaseline = 'middle';
191
+ ctx.fillText(this.formatTime(), this.x + 50, this.y + 28);
192
+
193
+ } else {
194
+ // iOS button
195
+ ctx.strokeStyle = '#C7C7CC';
196
+ ctx.lineWidth = 1;
197
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, 10);
198
+ ctx.stroke();
199
+
200
+ ctx.fillStyle = '#8E8E93';
201
+ ctx.font = '14px -apple-system, sans-serif';
202
+ ctx.textAlign = 'left';
203
+ ctx.textBaseline = 'middle';
204
+ ctx.fillText('Time', this.x + 15, this.y + 18);
205
+
206
+ ctx.fillStyle = '#000000';
207
+ ctx.font = '16px -apple-system, sans-serif';
208
+ ctx.fillText(this.formatTime(), this.x + 15, this.y + 38);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Dessine le picker Material (horloge)
214
+ * @private
215
+ */
216
+ drawMaterialPicker(ctx) {
217
+ const pickerY = this.y + this.height + 8;
218
+
219
+ // Background
220
+ ctx.fillStyle = '#FFFFFF';
221
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
222
+ ctx.shadowBlur = 16;
223
+ ctx.shadowOffsetY = 4;
224
+ ctx.fillRect(this.x, pickerY, this.width, this.pickerHeight);
225
+ ctx.shadowColor = 'transparent';
226
+
227
+ // Header avec time display
228
+ ctx.fillStyle = '#6200EE';
229
+ ctx.fillRect(this.x, pickerY, this.width, 80);
230
+
231
+ // Time display
232
+ const timeStr = `${String(this.hours).padStart(2, '0')}:${String(this.minutes).padStart(2, '0')}`;
233
+ ctx.fillStyle = '#FFFFFF';
234
+ ctx.font = 'bold 48px Roboto, sans-serif';
235
+ ctx.textAlign = 'center';
236
+ ctx.fillText(timeStr, this.x + this.width / 2, pickerY + 50);
237
+
238
+ // Mode selector (hours/minutes)
239
+ ctx.font = '14px Roboto, sans-serif';
240
+ const modeY = pickerY + 20;
241
+
242
+ if (this.mode === 'hours') {
243
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
244
+ } else {
245
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
246
+ }
247
+ ctx.fillText('Hours', this.x + this.width / 4, modeY);
248
+
249
+ if (this.mode === 'minutes') {
250
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
251
+ } else {
252
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
253
+ }
254
+ ctx.fillText('Minutes', this.x + 3 * this.width / 4, modeY);
255
+
256
+ // Clock
257
+ const centerX = this.x + this.width / 2;
258
+ const centerY = pickerY + 140 + 40;
259
+
260
+ // Clock circle
261
+ ctx.strokeStyle = '#E0E0E0';
262
+ ctx.lineWidth = 1;
263
+ ctx.beginPath();
264
+ ctx.arc(centerX, centerY, this.clockRadius, 0, Math.PI * 2);
265
+ ctx.stroke();
266
+
267
+ // Numbers
268
+ const count = this.mode === 'hours' ? 12 : 12;
269
+ const step = this.mode === 'hours' ? 1 : 5;
270
+
271
+ for (let i = 0; i < count; i++) {
272
+ const angle = (i * 2 * Math.PI / count) - Math.PI / 2;
273
+ const x = centerX + Math.cos(angle) * (this.clockRadius - 20);
274
+ const y = centerY + Math.sin(angle) * (this.clockRadius - 20);
275
+ const value = this.mode === 'hours' ? (i === 0 ? 12 : i) : i * step;
276
+
277
+ const isSelected = this.mode === 'hours'
278
+ ? value === this.hours
279
+ : value === Math.floor(this.minutes / 5) * 5;
280
+
281
+ if (isSelected) {
282
+ ctx.fillStyle = '#6200EE';
283
+ ctx.beginPath();
284
+ ctx.arc(x, y, 18, 0, Math.PI * 2);
285
+ ctx.fill();
286
+ ctx.fillStyle = '#FFFFFF';
287
+ } else {
288
+ ctx.fillStyle = '#000000';
289
+ }
290
+
291
+ ctx.font = '14px Roboto, sans-serif';
292
+ ctx.textAlign = 'center';
293
+ ctx.textBaseline = 'middle';
294
+ ctx.fillText(String(value), x, y);
295
+ }
296
+
297
+ // Hand (aiguille)
298
+ const currentValue = this.mode === 'hours' ? this.hours : this.minutes;
299
+ const handAngle = this.mode === 'hours'
300
+ ? ((currentValue % 12) * 2 * Math.PI / 12) - Math.PI / 2
301
+ : (currentValue * 2 * Math.PI / 60) - Math.PI / 2;
302
+
303
+ const handEndX = centerX + Math.cos(handAngle) * (this.clockRadius - 20);
304
+ const handEndY = centerY + Math.sin(handAngle) * (this.clockRadius - 20);
305
+
306
+ ctx.strokeStyle = '#6200EE';
307
+ ctx.lineWidth = 2;
308
+ ctx.beginPath();
309
+ ctx.moveTo(centerX, centerY);
310
+ ctx.lineTo(handEndX, handEndY);
311
+ ctx.stroke();
312
+
313
+ // Center dot
314
+ ctx.fillStyle = '#6200EE';
315
+ ctx.beginPath();
316
+ ctx.arc(centerX, centerY, 4, 0, Math.PI * 2);
317
+ ctx.fill();
318
+
319
+ // OK button
320
+ const okY = pickerY + this.pickerHeight - 50;
321
+ ctx.fillStyle = '#6200EE';
322
+ ctx.font = 'bold 14px Roboto, sans-serif';
323
+ ctx.textAlign = 'right';
324
+ ctx.fillText('OK', this.x + this.width - 30, okY + 20);
325
+ }
326
+
327
+ /**
328
+ * Dessine le picker iOS (wheel)
329
+ * @private
330
+ */
331
+ drawIOSPicker(ctx) {
332
+ const pickerY = this.y + this.height + 8;
333
+
334
+ // Background
335
+ ctx.fillStyle = '#F9F9F9';
336
+ this.roundRect(ctx, this.x, pickerY, this.width, this.pickerHeight, 12);
337
+ ctx.fill();
338
+
339
+ // Selection bar (au milieu)
340
+ const selectionY = pickerY + this.pickerHeight / 2 - 20;
341
+ ctx.fillStyle = 'rgba(0, 122, 255, 0.1)';
342
+ this.roundRect(ctx, this.x + 20, selectionY, this.width - 40, 40, 8);
343
+ ctx.fill();
344
+
345
+ // Wheels (heures et minutes)
346
+ const hourWheelX = this.x + this.width / 3;
347
+ const minuteWheelX = this.x + 2 * this.width / 3;
348
+ const wheelY = pickerY + this.pickerHeight / 2;
349
+
350
+ // Heures
351
+ this.drawWheel(ctx, hourWheelX, wheelY, this.hours, 23, false);
352
+
353
+ // Séparateur ":"
354
+ ctx.fillStyle = '#000000';
355
+ ctx.font = '24px -apple-system, sans-serif';
356
+ ctx.textAlign = 'center';
357
+ ctx.fillText(':', this.x + this.width / 2, wheelY);
358
+
359
+ // Minutes
360
+ this.drawWheel(ctx, minuteWheelX, wheelY, this.minutes, 59, true);
361
+
362
+ // Done button
363
+ const doneY = pickerY + this.pickerHeight - 44;
364
+ ctx.fillStyle = '#007AFF';
365
+ ctx.font = '17px -apple-system, sans-serif';
366
+ ctx.textAlign = 'center';
367
+ ctx.fillText('Done', this.x + this.width / 2, doneY + 22);
368
+ }
369
+
370
+ /**
371
+ * Dessine une roue iOS
372
+ * @private
373
+ */
374
+ drawWheel(ctx, x, y, selected, max, isMinute) {
375
+ const itemHeight = 40;
376
+ const visibleItems = 5;
377
+
378
+ for (let i = -2; i <= 2; i++) {
379
+ let value = (selected + i + max + 1) % (max + 1);
380
+ if (isMinute) value = Math.floor(value / 5) * 5;
381
+
382
+ const itemY = y + i * itemHeight;
383
+ const opacity = 1 - Math.abs(i) * 0.4;
384
+ const scale = 1 - Math.abs(i) * 0.2;
385
+
386
+ ctx.save();
387
+ ctx.globalAlpha = opacity;
388
+ ctx.fillStyle = i === 0 ? '#000000' : '#8E8E93';
389
+ ctx.font = `${Math.floor(24 * scale)}px -apple-system, sans-serif`;
390
+ ctx.textAlign = 'center';
391
+ ctx.textBaseline = 'middle';
392
+ ctx.fillText(String(value).padStart(2, '0'), x, itemY);
393
+ ctx.restore();
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Formate l'heure
399
+ * @private
400
+ */
401
+ formatTime() {
402
+ const h = String(this.hours).padStart(2, '0');
403
+ const m = String(this.minutes).padStart(2, '0');
404
+ return `${h}:${m}`;
405
+ }
406
+
407
+ /**
408
+ * Rectangle arrondi
409
+ * @private
410
+ */
411
+ roundRect(ctx, x, y, width, height, radius) {
412
+ ctx.beginPath();
413
+ ctx.moveTo(x + radius, y);
414
+ ctx.lineTo(x + width - radius, y);
415
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
416
+ ctx.lineTo(x + width, y + height - radius);
417
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
418
+ ctx.lineTo(x + radius, y + height);
419
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
420
+ ctx.lineTo(x, y + radius);
421
+ ctx.quadraticCurveTo(x, y, x + radius, y);
422
+ }
423
+
424
+ /**
425
+ * Vérifie si point dans limites
426
+ */
427
+ isPointInside(x, y) {
428
+ if (x >= this.x && x <= this.x + this.width &&
429
+ y >= this.y && y <= this.y + this.height) {
430
+ return true;
431
+ }
432
+
433
+ if (this.isOpen) {
434
+ const pickerY = this.y + this.height + 8;
435
+ return x >= this.x && x <= this.x + this.width &&
436
+ y >= pickerY && y <= pickerY + this.pickerHeight;
437
+ }
438
+
439
+ return false;
440
+ }
441
+ }
442
+
443
+ export default TimePicker;
@@ -6,6 +6,8 @@ import Text from '../components/Text.js';
6
6
  import View from '../components/View.js';
7
7
  import Card from '../components/Card.js';
8
8
  import FAB from '../components/FAB.js';
9
+ import SpeedDialFAB from '../components/SpeedDialFAB.js';
10
+ import MorphingFAB from '../components/MorphingFAB.js';
9
11
  import CircularProgress from '../components/CircularProgress.js';
10
12
  import ImageComponent from '../components/ImageComponent.js';
11
13
  import DatePicker from '../components/DatePicker.js';
@@ -45,6 +47,8 @@ import Table from '../components/Table.js';
45
47
  import TreeView from '../components/TreeView.js';
46
48
  import SearchInput from '../components/SearchInput.js';
47
49
  import ImageCarousel from '../components/ImageCarousel.js';
50
+ import PasswordInput from '../components/PasswordInput.js';
51
+ import InputTags from '../components/InputTags.js';
48
52
 
49
53
  // Utils
50
54
  import SafeArea from '../utils/SafeArea.js';
@@ -866,9 +870,9 @@ class CanvasFramework {
866
870
  if (child.pressed) {
867
871
  child.pressed = false;
868
872
 
869
- if (child instanceof Input) {
873
+ if (child instanceof Input || child instanceof PasswordInput || child instanceof InputTags) {
870
874
  for (let other of this.components) {
871
- if (other instanceof Input && other !== child && other.focused) {
875
+ if (other instanceof Input || other instanceof PasswordInput || other instanceof InputTags && other !== child && other.focused) {
872
876
  other.focused = false;
873
877
  other.cursorVisible = false;
874
878
  if (other.onBlur) other.onBlur();
@@ -912,9 +916,9 @@ class CanvasFramework {
912
916
  if (comp.pressed) {
913
917
  comp.pressed = false;
914
918
 
915
- if (comp instanceof Input) {
919
+ if (comp instanceof Input || comp instanceof PasswordInput || comp instanceof InputTags) {
916
920
  for (let other of this.components) {
917
- if (other instanceof Input && other !== comp && other.focused) {
921
+ if (other instanceof Input || other instanceof PasswordInput || other instanceof InputTags && other !== comp && other.focused) {
918
922
  other.focused = false;
919
923
  other.cursorVisible = false;
920
924
  if (other.onBlur) other.onBlur();
package/index.js CHANGED
@@ -13,6 +13,8 @@ export { default as Text } from './components/Text.js';
13
13
  export { default as View } from './components/View.js';
14
14
  export { default as Card } from './components/Card.js';
15
15
  export { default as FAB } from './components/FAB.js';
16
+ export { default as SpeedDialFAB } from './components/SpeedDialFAB.js';
17
+ export { default as MorphingFAB } from './components/MorphingFAB.js';
16
18
  export { default as CircularProgress } from './components/CircularProgress.js';
17
19
  export { default as ImageComponent } from './components/ImageComponent.js';
18
20
  export { default as DatePicker } from './components/DatePicker.js';
@@ -52,6 +54,8 @@ export { default as Table } from './components/Table.js';
52
54
  export { default as TreeView } from './components/TreeView.js';
53
55
  export { default as SearchInput } from './components/SearchInput.js';
54
56
  export { default as ImageCarousel } from './components/ImageCarousel.js';
57
+ export { default as PasswordInput } from './components/PasswordInput.js';
58
+ export { default as InputTags } from './components/InputTags.js';
55
59
 
56
60
  // Utils
57
61
  export { default as SafeArea } from './utils/SafeArea.js';
@@ -91,7 +95,7 @@ export { default as FeatureFlags } from './manager/FeatureFlags.js';
91
95
 
92
96
  // Version du framework
93
97
 
94
- export const VERSION = '0.3.8';
98
+ export const VERSION = '0.3.9';
95
99
 
96
100
 
97
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",