canvasframework 0.5.46 → 0.5.48
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.
- package/components/Accordion.js +2 -1
- package/components/Banner.js +140 -183
- package/components/Chip.js +126 -145
- package/components/RadioButton.js +132 -77
- package/components/SegmentedControl.js +7 -5
- package/components/Stepper.js +255 -230
- package/package.json +1 -1
package/components/Accordion.js
CHANGED
package/components/Banner.js
CHANGED
|
@@ -13,7 +13,6 @@ export default class Banner extends Component {
|
|
|
13
13
|
this.platform = framework.platform || 'material';
|
|
14
14
|
|
|
15
15
|
this.width = options.width || framework.width || window.innerWidth;
|
|
16
|
-
this.height = options.height || 64;
|
|
17
16
|
this.x = options.x || 0;
|
|
18
17
|
this.y = options.y || 0;
|
|
19
18
|
|
|
@@ -24,26 +23,25 @@ export default class Banner extends Component {
|
|
|
24
23
|
this._lastUpdate = performance.now();
|
|
25
24
|
this._colors = this._resolveColors();
|
|
26
25
|
|
|
27
|
-
// Bounds calculées à chaque frame
|
|
28
26
|
this._actionBounds = [];
|
|
29
27
|
this._dismissBounds = null;
|
|
30
|
-
|
|
31
|
-
// Pour indiquer qu'on gère nos propres clics
|
|
32
|
-
this.selfManagedClicks = true;
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
this.
|
|
29
|
+
this.selfManagedClicks = true;
|
|
30
|
+
this.ripples = [];
|
|
36
31
|
|
|
37
|
-
// Ref si fourni
|
|
38
32
|
if (options.ref) options.ref.current = this;
|
|
33
|
+
|
|
34
|
+
this._calculateHeight();
|
|
35
|
+
this._setupEventListeners();
|
|
36
|
+
|
|
37
|
+
this.bgColor = options.bgColor || null;
|
|
38
|
+
this.textColor = options.textColor || null;
|
|
39
|
+
this.buttonColor = options.buttonColor || null;
|
|
40
|
+
this.rippleColor = options.rippleColor || 'rgba(26,115,232,0.2)';
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
/* ===================== Setup ===================== */
|
|
42
43
|
_setupEventListeners() {
|
|
43
|
-
// Stocker les références pour pouvoir les retirer plus tard
|
|
44
44
|
this._boundHandleClick = this._handleClick.bind(this);
|
|
45
|
-
|
|
46
|
-
// Écouter les événements sur le canvas parent
|
|
47
45
|
if (this.framework && this.framework.canvas) {
|
|
48
46
|
this.framework.canvas.addEventListener('click', this._boundHandleClick);
|
|
49
47
|
this.framework.canvas.addEventListener('touchend', this._boundHandleClick);
|
|
@@ -57,53 +55,66 @@ export default class Banner extends Component {
|
|
|
57
55
|
}
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this._setupEventListeners();
|
|
63
|
-
}
|
|
58
|
+
onMount() { this._setupEventListeners(); }
|
|
59
|
+
onUnmount() { this._removeEventListeners(); }
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
_resolveColors() {
|
|
62
|
+
if (this.platform === 'cupertino') {
|
|
63
|
+
return {
|
|
64
|
+
bg: this.bgColor || 'rgba(250,250,250,0.95)',
|
|
65
|
+
fg: this.textColor || '#000',
|
|
66
|
+
accent: this.buttonColor || '#007AFF',
|
|
67
|
+
divider: 'rgba(60,60,67,0.15)'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const map = {
|
|
72
|
+
info: '#E8F0FE',
|
|
73
|
+
success: '#E6F4EA',
|
|
74
|
+
warning: '#FEF7E0',
|
|
75
|
+
error: '#FCE8E6'
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
bg: this.bgColor || map[this.type] || map.info,
|
|
80
|
+
fg: this.textColor || '#1F1F1F',
|
|
81
|
+
accent: this.buttonColor || '#1A73E8'
|
|
82
|
+
};
|
|
67
83
|
}
|
|
68
84
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
divider: 'rgba(60,60,67,0.15)'
|
|
77
|
-
};
|
|
78
|
-
}
|
|
85
|
+
_calculateHeight() {
|
|
86
|
+
const ctx = this.framework.ctx;
|
|
87
|
+
ctx.save();
|
|
88
|
+
ctx.font =
|
|
89
|
+
this.platform === 'cupertino'
|
|
90
|
+
? '600 15px -apple-system, SF Pro Display'
|
|
91
|
+
: '400 14px Roboto, sans-serif';
|
|
79
92
|
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
const maxWidth = this.width - 32; // padding 16px de chaque côté
|
|
94
|
+
const words = this.text.split(' ');
|
|
95
|
+
let lines = [];
|
|
96
|
+
let line = '';
|
|
97
|
+
|
|
98
|
+
words.forEach(word => {
|
|
99
|
+
const test = line + word + ' ';
|
|
100
|
+
if (ctx.measureText(test).width < maxWidth) {
|
|
101
|
+
line = test;
|
|
102
|
+
} else {
|
|
103
|
+
lines.push(line);
|
|
104
|
+
line = word + ' ';
|
|
105
|
+
}
|
|
106
|
+
});
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
fg: '#1F1F1F',
|
|
91
|
-
accent: '#1A73E8'
|
|
92
|
-
};
|
|
93
|
-
}
|
|
108
|
+
lines.push(line);
|
|
109
|
+
this._lines = lines;
|
|
94
110
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
this.visible = true;
|
|
98
|
-
this.markDirty();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
hide() {
|
|
102
|
-
this.visible = false;
|
|
103
|
-
this.markDirty();
|
|
111
|
+
ctx.restore();
|
|
112
|
+
this.height = Math.max(64, lines.length * 20 + 16); // minimum 64px
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
|
|
115
|
+
show() { this.visible = true; this.markDirty(); }
|
|
116
|
+
hide() { this.visible = false; this.markDirty(); }
|
|
117
|
+
|
|
107
118
|
update() {
|
|
108
119
|
const now = performance.now();
|
|
109
120
|
const dt = Math.min((now - this._lastUpdate) / 16.6, 3);
|
|
@@ -113,27 +124,53 @@ export default class Banner extends Component {
|
|
|
113
124
|
this.progress = Math.max(0, Math.min(1, this.progress));
|
|
114
125
|
|
|
115
126
|
if (Math.abs(target - this.progress) > 0.01) this.markDirty();
|
|
116
|
-
|
|
117
127
|
this._lastUpdate = now;
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
|
|
130
|
+
addRipple(x, y) {
|
|
131
|
+
if (this.platform !== 'material') return;
|
|
132
|
+
|
|
133
|
+
this.ripples.push({
|
|
134
|
+
x, y,
|
|
135
|
+
radius: 0,
|
|
136
|
+
maxRadius: Math.max(this.width, this.height),
|
|
137
|
+
opacity: 0.3
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this.animateRipples();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
animateRipples() {
|
|
144
|
+
const animate = () => {
|
|
145
|
+
let active = false;
|
|
146
|
+
for (let r of this.ripples) {
|
|
147
|
+
if (r.radius < r.maxRadius) {
|
|
148
|
+
r.radius += r.maxRadius / 15;
|
|
149
|
+
r.opacity -= 0.03;
|
|
150
|
+
active = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
this.ripples = this.ripples.filter(r => r.opacity > 0);
|
|
154
|
+
if (active) requestAnimationFrame(animate);
|
|
155
|
+
};
|
|
156
|
+
animate();
|
|
157
|
+
}
|
|
158
|
+
|
|
121
159
|
draw(ctx) {
|
|
122
160
|
this.update();
|
|
123
161
|
if (this.progress <= 0.01) return;
|
|
124
162
|
|
|
125
|
-
const
|
|
126
|
-
const visibleHeight = h;
|
|
127
|
-
|
|
163
|
+
const visibleHeight = this.height * this.progress;
|
|
128
164
|
ctx.save();
|
|
129
165
|
|
|
130
|
-
//
|
|
166
|
+
// Shadow pour Material
|
|
131
167
|
if (this.platform === 'material') {
|
|
132
|
-
ctx.shadowColor = 'rgba(0,0,0,0.
|
|
133
|
-
ctx.shadowBlur =
|
|
168
|
+
ctx.shadowColor = 'rgba(0,0,0,0.12)';
|
|
169
|
+
ctx.shadowBlur = 6;
|
|
134
170
|
ctx.shadowOffsetY = 2;
|
|
135
171
|
}
|
|
136
172
|
|
|
173
|
+
// Background
|
|
137
174
|
ctx.fillStyle = this._colors.bg;
|
|
138
175
|
ctx.fillRect(this.x, this.y, this.width, visibleHeight);
|
|
139
176
|
ctx.shadowColor = 'transparent';
|
|
@@ -147,20 +184,39 @@ export default class Banner extends Component {
|
|
|
147
184
|
ctx.stroke();
|
|
148
185
|
}
|
|
149
186
|
|
|
187
|
+
// Ripple Material
|
|
188
|
+
if (this.platform === 'material') {
|
|
189
|
+
ctx.save();
|
|
190
|
+
ctx.beginPath();
|
|
191
|
+
ctx.rect(this.x, this.y, this.width, visibleHeight);
|
|
192
|
+
ctx.clip();
|
|
193
|
+
|
|
194
|
+
this.ripples.forEach(r => {
|
|
195
|
+
ctx.globalAlpha = r.opacity;
|
|
196
|
+
ctx.fillStyle = this.rippleColor;
|
|
197
|
+
ctx.beginPath();
|
|
198
|
+
ctx.arc(this.x + r.x, this.y + r.y, r.radius, 0, Math.PI * 2);
|
|
199
|
+
ctx.fill();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
ctx.restore();
|
|
203
|
+
ctx.globalAlpha = 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
150
206
|
// Text
|
|
151
207
|
ctx.fillStyle = this._colors.fg;
|
|
152
208
|
ctx.font =
|
|
153
209
|
this.platform === 'cupertino'
|
|
154
|
-
? '
|
|
210
|
+
? '600 15px -apple-system, SF Pro Display'
|
|
155
211
|
: '400 14px Roboto, sans-serif';
|
|
156
|
-
ctx.textBaseline = 'middle';
|
|
157
212
|
ctx.textAlign = 'left';
|
|
213
|
+
ctx.textBaseline = 'middle';
|
|
214
|
+
|
|
158
215
|
ctx.fillText(this.text, this.x + 16, this.y + visibleHeight / 2);
|
|
159
216
|
|
|
160
|
-
// Actions
|
|
217
|
+
// Actions
|
|
161
218
|
this._actionBounds = [];
|
|
162
219
|
let x = this.width - 16;
|
|
163
|
-
|
|
164
220
|
for (let i = this.actions.length - 1; i >= 0; i--) {
|
|
165
221
|
const action = this.actions[i];
|
|
166
222
|
const textWidth = ctx.measureText(action.label).width + 20;
|
|
@@ -171,15 +227,9 @@ export default class Banner extends Component {
|
|
|
171
227
|
ctx.textBaseline = 'middle';
|
|
172
228
|
ctx.fillText(action.label, this.x + x + textWidth / 2, this.y + visibleHeight / 2);
|
|
173
229
|
|
|
174
|
-
// Stocker la hitbox (en coordonnées écran, pas canvas)
|
|
175
230
|
this._actionBounds.push({
|
|
176
231
|
action: action,
|
|
177
|
-
bounds: {
|
|
178
|
-
x: this.x + x,
|
|
179
|
-
y: this.y + (visibleHeight - 44) / 2,
|
|
180
|
-
w: textWidth,
|
|
181
|
-
h: 44
|
|
182
|
-
}
|
|
232
|
+
bounds: { x: this.x + x, y: this.y + (visibleHeight - 44)/2, w: textWidth, h: 44 }
|
|
183
233
|
});
|
|
184
234
|
|
|
185
235
|
x -= 12;
|
|
@@ -191,152 +241,59 @@ export default class Banner extends Component {
|
|
|
191
241
|
const cx = this.width - 28;
|
|
192
242
|
const cy = this.y + visibleHeight / 2;
|
|
193
243
|
|
|
194
|
-
ctx.fillStyle =
|
|
195
|
-
|
|
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';
|
|
244
|
+
ctx.fillStyle = this.platform === 'cupertino' ? 'rgba(60,60,67,0.6)' : this._colors.fg;
|
|
245
|
+
ctx.font = this.platform === 'cupertino' ? '600 16px -apple-system' : '500 16px Roboto';
|
|
203
246
|
ctx.textAlign = 'center';
|
|
204
247
|
ctx.textBaseline = 'middle';
|
|
205
248
|
ctx.fillText('×', cx, cy);
|
|
206
249
|
|
|
207
|
-
this._dismissBounds = {
|
|
208
|
-
x: cx - hitSize / 2,
|
|
209
|
-
y: cy - hitSize / 2,
|
|
210
|
-
w: hitSize,
|
|
211
|
-
h: hitSize
|
|
212
|
-
};
|
|
250
|
+
this._dismissBounds = { x: cx - hitSize/2, y: cy - hitSize/2, w: hitSize, h: hitSize };
|
|
213
251
|
} else {
|
|
214
252
|
this._dismissBounds = null;
|
|
215
253
|
}
|
|
216
254
|
|
|
217
255
|
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
256
|
}
|
|
260
257
|
|
|
261
|
-
/* ===================== Click Handling ===================== */
|
|
262
258
|
_handleClick(event) {
|
|
263
259
|
if (this.progress < 0.95) return;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
let clientX, clientY;
|
|
267
|
-
|
|
260
|
+
|
|
261
|
+
let clientX = event.clientX, clientY = event.clientY;
|
|
268
262
|
if (event.type === 'touchend') {
|
|
269
263
|
const touch = event.changedTouches[0];
|
|
270
264
|
clientX = touch.clientX;
|
|
271
265
|
clientY = touch.clientY;
|
|
272
|
-
} else {
|
|
273
|
-
clientX = event.clientX;
|
|
274
|
-
clientY = event.clientY;
|
|
275
266
|
}
|
|
276
|
-
|
|
277
|
-
// Convertir en coordonnées canvas SIMPLIFIÉ
|
|
267
|
+
|
|
278
268
|
const canvasRect = this.framework.canvas.getBoundingClientRect();
|
|
279
|
-
|
|
280
|
-
// Coordonnées relatives au canvas (en pixels CSS, pas en pixels canvas)
|
|
281
269
|
const x = clientX - canvasRect.left;
|
|
282
270
|
const y = clientY - canvasRect.top;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
271
|
+
|
|
272
|
+
// Ripple effect sur banner
|
|
273
|
+
if (this.platform === 'material') this.addRipple(x - this.x, y - this.y);
|
|
274
|
+
|
|
275
|
+
if (x < this.x || x > this.x + this.width || y < this.y || y > this.y + this.height) return;
|
|
276
|
+
|
|
305
277
|
event.stopPropagation();
|
|
306
|
-
|
|
307
|
-
//
|
|
278
|
+
|
|
279
|
+
// Dismiss
|
|
308
280
|
if (this.dismissible && this._dismissBounds) {
|
|
309
281
|
const b = this._dismissBounds;
|
|
310
|
-
|
|
311
|
-
if (x >= b.x && x <= b.x + b.w &&
|
|
312
|
-
y >= b.y && y <= b.y + b.h) {
|
|
313
|
-
console.log('Dismiss clicked!');
|
|
282
|
+
if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
|
|
314
283
|
this.hide();
|
|
315
284
|
return;
|
|
316
285
|
}
|
|
317
286
|
}
|
|
318
|
-
|
|
319
|
-
//
|
|
320
|
-
if (this._actionBounds
|
|
321
|
-
console.log('Checking', this._actionBounds.length, 'action bounds');
|
|
287
|
+
|
|
288
|
+
// Actions
|
|
289
|
+
if (this._actionBounds.length) {
|
|
322
290
|
for (const item of this._actionBounds) {
|
|
323
291
|
const b = item.bounds;
|
|
324
|
-
|
|
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);
|
|
292
|
+
if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
|
|
328
293
|
item.action.onClick?.();
|
|
329
294
|
return;
|
|
330
295
|
}
|
|
331
296
|
}
|
|
332
297
|
}
|
|
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
298
|
}
|
|
342
|
-
}
|
|
299
|
+
}
|