@fuzo/soccer-board 0.1.0-alpha.2 → 0.2.0-alpha.2

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,6 +1,6 @@
1
+ import { isPlatformBrowser } from '@angular/common';
1
2
  import * as i0 from '@angular/core';
2
3
  import { inject, ElementRef, input, output, signal, computed, effect, HostListener, Component, PLATFORM_ID, viewChild, HostBinding } from '@angular/core';
3
- import { isPlatformBrowser } from '@angular/common';
4
4
  import html2canvas from 'html2canvas-pro';
5
5
 
6
6
  /**
@@ -201,6 +201,10 @@ class SoccerBoardPlayerComponent {
201
201
  // Inputs using signals
202
202
  player = input.required(...(ngDevMode ? [{ debugName: "player" }] : []));
203
203
  size = input(SoccerBoardPlayerSize.Medium, ...(ngDevMode ? [{ debugName: "size" }] : []));
204
+ // Position inputs - when provided, the component positions itself
205
+ // These are percentage values (0-100) relative to parent container
206
+ positionLeft = input(null, ...(ngDevMode ? [{ debugName: "positionLeft" }] : []));
207
+ positionTop = input(null, ...(ngDevMode ? [{ debugName: "positionTop" }] : []));
204
208
  // Outputs
205
209
  dragStart = output();
206
210
  dragging = output();
@@ -211,21 +215,14 @@ class SoccerBoardPlayerComponent {
211
215
  isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
212
216
  dragOffsetX = signal(0, ...(ngDevMode ? [{ debugName: "dragOffsetX" }] : []));
213
217
  dragOffsetY = signal(0, ...(ngDevMode ? [{ debugName: "dragOffsetY" }] : []));
214
- // Computed drag styles
215
- dragStyles = computed(() => {
216
- if (this.isDragging()) {
217
- return {
218
- opacity: '0.7',
219
- cursor: 'grabbing',
220
- zIndex: '1000',
221
- };
222
- }
223
- return {
224
- opacity: '1',
225
- cursor: 'grab',
226
- zIndex: '',
227
- };
228
- }, ...(ngDevMode ? [{ debugName: "dragStyles" }] : []));
218
+ // Track if we're waiting for position update after drag
219
+ waitingForPositionUpdate = signal(false, ...(ngDevMode ? [{ debugName: "waitingForPositionUpdate" }] : []));
220
+ lastDragEndPosition = signal(null, ...(ngDevMode ? [{ debugName: "lastDragEndPosition" }] : []));
221
+ // Track if drag has actually started (mouse moved after mousedown)
222
+ isPotentialDrag = signal(false, ...(ngDevMode ? [{ debugName: "isPotentialDrag" }] : []));
223
+ dragStartX = signal(0, ...(ngDevMode ? [{ debugName: "dragStartX" }] : []));
224
+ dragStartY = signal(0, ...(ngDevMode ? [{ debugName: "dragStartY" }] : []));
225
+ DRAG_THRESHOLD = 5;
229
226
  // Computed values
230
227
  playerPhoto = computed(() => {
231
228
  const photo = this.player()?.photo;
@@ -238,6 +235,10 @@ class SoccerBoardPlayerComponent {
238
235
  const player = this.player();
239
236
  return player?.fieldX !== undefined && player?.fieldY !== undefined;
240
237
  }, ...(ngDevMode ? [{ debugName: "isOnField" }] : []));
238
+ // Determine if this component should position itself (has position inputs)
239
+ hasPosition = computed(() => {
240
+ return this.positionLeft() !== null && this.positionTop() !== null;
241
+ }, ...(ngDevMode ? [{ debugName: "hasPosition" }] : []));
241
242
  // Show fieldPosition if on field, otherwise show preferredPosition
242
243
  playerPosition = computed(() => {
243
244
  const player = this.player();
@@ -259,62 +260,151 @@ class SoccerBoardPlayerComponent {
259
260
  }
260
261
  }, ...(ngDevMode ? [{ debugName: "sizeClasses" }] : []));
261
262
  constructor() {
262
- // Effect to apply drag styles
263
+ // Effect to apply position styles when not dragging
264
+ effect(() => {
265
+ const element = this.hostElement.nativeElement;
266
+ const dragging = this.isDragging();
267
+ const waiting = this.waitingForPositionUpdate();
268
+ const left = this.positionLeft();
269
+ const top = this.positionTop();
270
+ const hasPos = this.hasPosition();
271
+ const lastPos = this.lastDragEndPosition();
272
+ // Don't update position styles while dragging
273
+ if (dragging) {
274
+ return;
275
+ }
276
+ // If we're waiting for position update after drag ended
277
+ if (waiting && lastPos !== null) {
278
+ // Check if position has changed from what it was at drag end
279
+ // If position changed, we received the update and can apply the new position
280
+ if (left !== lastPos.left || top !== lastPos.top) {
281
+ this.waitingForPositionUpdate.set(false);
282
+ this.lastDragEndPosition.set(null);
283
+ // Continue to apply the new position below
284
+ }
285
+ else {
286
+ // Still waiting for position update, keep the element in its current visual position
287
+ return;
288
+ }
289
+ }
290
+ if (hasPos && left !== null && top !== null) {
291
+ // Player is on the field - apply absolute positioning
292
+ element.style.position = 'absolute';
293
+ element.style.left = `${left}%`;
294
+ element.style.top = `${top}%`;
295
+ element.style.transform = 'translate(-50%, -50%)';
296
+ }
297
+ else {
298
+ // Player is in sidebar - clear positioning styles
299
+ element.style.position = '';
300
+ element.style.left = '';
301
+ element.style.top = '';
302
+ element.style.transform = '';
303
+ }
304
+ });
305
+ // Effect to apply drag visual styles (opacity, cursor, zIndex)
263
306
  effect(() => {
264
- const styles = this.dragStyles();
307
+ const dragging = this.isDragging();
265
308
  const element = this.hostElement.nativeElement;
266
- element.style.opacity = styles.opacity;
267
- element.style.cursor = styles.cursor;
268
- element.style.zIndex = styles.zIndex;
309
+ if (dragging) {
310
+ element.style.opacity = '0.8';
311
+ element.style.cursor = 'grabbing';
312
+ element.style.zIndex = '1000';
313
+ }
314
+ else {
315
+ element.style.opacity = '';
316
+ element.style.cursor = '';
317
+ element.style.zIndex = '';
318
+ }
269
319
  });
270
320
  }
271
321
  onMouseDown(event) {
322
+ const target = event.target;
323
+ if (target.closest('.remove-badge')) {
324
+ return;
325
+ }
272
326
  event.preventDefault();
273
- this.startDrag(event.clientX, event.clientY);
327
+ this.preparePotentialDrag(event.clientX, event.clientY);
274
328
  }
275
329
  onTouchStart(event) {
276
330
  if (event.touches.length !== 1)
277
331
  return;
332
+ const target = event.target;
333
+ if (target.closest('.remove-badge')) {
334
+ return;
335
+ }
278
336
  event.preventDefault();
279
337
  const touch = event.touches[0];
280
- this.startDrag(touch.clientX, touch.clientY);
338
+ this.preparePotentialDrag(touch.clientX, touch.clientY);
281
339
  }
282
- startDrag(clientX, clientY) {
283
- console.log('[Player] Start drag');
340
+ preparePotentialDrag(clientX, clientY) {
341
+ this.dragStartX.set(clientX);
342
+ this.dragStartY.set(clientY);
343
+ this.isPotentialDrag.set(true);
284
344
  const element = this.hostElement.nativeElement;
285
345
  const rect = element.getBoundingClientRect();
286
346
  this.dragOffsetX.set(clientX - rect.left);
287
347
  this.dragOffsetY.set(clientY - rect.top);
348
+ }
349
+ startDrag(clientX, clientY) {
350
+ const element = this.hostElement.nativeElement;
351
+ const rect = element.getBoundingClientRect();
352
+ // IMPORTANT: Set dragging state FIRST
288
353
  this.isDragging.set(true);
354
+ // Apply fixed position for smooth dragging
355
+ // Use exact current visual position to prevent any jump
356
+ element.style.position = 'fixed';
357
+ element.style.left = `${rect.left}px`;
358
+ element.style.top = `${rect.top}px`;
359
+ element.style.width = `${rect.width}px`;
360
+ element.style.height = `${rect.height}px`;
361
+ element.style.transform = 'none'; // Remove translate during drag
362
+ element.style.margin = '0';
363
+ element.style.pointerEvents = 'none';
289
364
  const player = this.player();
290
- console.log('[Player] Emitting dragStart', { playerId: player.id, playerName: player.name });
291
365
  this.dragStart.emit({
292
366
  playerId: player.id,
293
367
  playerName: player.name,
294
368
  });
295
369
  }
296
370
  onMouseMove(event) {
371
+ if (this.isPotentialDrag() && !this.isDragging()) {
372
+ const deltaX = Math.abs(event.clientX - this.dragStartX());
373
+ const deltaY = Math.abs(event.clientY - this.dragStartY());
374
+ if (deltaX > this.DRAG_THRESHOLD || deltaY > this.DRAG_THRESHOLD) {
375
+ this.startDrag(event.clientX, event.clientY);
376
+ }
377
+ return;
378
+ }
297
379
  if (!this.isDragging())
298
380
  return;
299
381
  event.preventDefault();
300
382
  this.handleDrag(event.clientX, event.clientY);
301
383
  }
302
384
  onTouchMove(event) {
303
- if (!this.isDragging() || event.touches.length !== 1)
385
+ if (event.touches.length !== 1)
304
386
  return;
305
- event.preventDefault();
306
387
  const touch = event.touches[0];
388
+ if (this.isPotentialDrag() && !this.isDragging()) {
389
+ const deltaX = Math.abs(touch.clientX - this.dragStartX());
390
+ const deltaY = Math.abs(touch.clientY - this.dragStartY());
391
+ if (deltaX > this.DRAG_THRESHOLD || deltaY > this.DRAG_THRESHOLD) {
392
+ this.startDrag(touch.clientX, touch.clientY);
393
+ }
394
+ return;
395
+ }
396
+ if (!this.isDragging())
397
+ return;
398
+ event.preventDefault();
307
399
  this.handleDrag(touch.clientX, touch.clientY);
308
400
  }
309
401
  handleDrag(clientX, clientY) {
310
402
  const element = this.hostElement.nativeElement;
311
403
  const offsetX = this.dragOffsetX();
312
404
  const offsetY = this.dragOffsetY();
313
- // Move visually during drag
314
- element.style.position = 'fixed';
405
+ // Update position during drag
315
406
  element.style.left = `${clientX - offsetX}px`;
316
407
  element.style.top = `${clientY - offsetY}px`;
317
- element.style.pointerEvents = 'none';
318
408
  const player = this.player();
319
409
  this.dragging.emit({
320
410
  playerId: player.id,
@@ -325,59 +415,100 @@ class SoccerBoardPlayerComponent {
325
415
  });
326
416
  }
327
417
  onMouseUp(event) {
418
+ if (this.isPotentialDrag() && !this.isDragging()) {
419
+ this.isPotentialDrag.set(false);
420
+ return;
421
+ }
328
422
  if (!this.isDragging())
329
423
  return;
330
424
  this.endDrag(event.clientX, event.clientY);
331
425
  }
332
426
  onTouchEnd(event) {
427
+ if (this.isPotentialDrag() && !this.isDragging()) {
428
+ this.isPotentialDrag.set(false);
429
+ return;
430
+ }
333
431
  if (!this.isDragging())
334
432
  return;
335
433
  const touch = event.changedTouches[0];
336
434
  this.endDrag(touch.clientX, touch.clientY);
337
435
  }
338
436
  endDrag(clientX, clientY) {
339
- console.log('[Player] End drag', { clientX, clientY });
340
- this.isDragging.set(false);
341
437
  const element = this.hostElement.nativeElement;
342
- element.style.pointerEvents = '';
343
- element.style.position = '';
344
- element.style.left = '';
345
- element.style.top = '';
346
438
  const player = this.player();
439
+ const rect = element.getBoundingClientRect();
440
+ // Store current position inputs to detect when they change
441
+ const currentLeft = this.positionLeft();
442
+ const currentTop = this.positionTop();
443
+ // Emit event with all necessary information
347
444
  const dragEndEvent = {
348
445
  playerId: player.id,
349
446
  clientX,
350
447
  clientY,
351
448
  offsetX: this.dragOffsetX(),
352
449
  offsetY: this.dragOffsetY(),
450
+ elementWidth: rect.width,
451
+ elementHeight: rect.height,
353
452
  };
354
- console.log('[Player] Emitting dragEnd', dragEndEvent);
355
453
  this.dragEnd.emit(dragEndEvent);
454
+ // Mark that we're waiting for position update
455
+ // Store the current position so we can detect when it changes
456
+ if (currentLeft !== null && currentTop !== null) {
457
+ this.waitingForPositionUpdate.set(true);
458
+ this.lastDragEndPosition.set({ left: currentLeft, top: currentTop });
459
+ }
460
+ // Clear drag state
461
+ this.isDragging.set(false);
462
+ this.isPotentialDrag.set(false);
463
+ // Clear drag-specific styles except position-related ones
464
+ // Position will be handled by the effect when new position arrives
465
+ element.style.width = '';
466
+ element.style.height = '';
467
+ element.style.margin = '';
468
+ element.style.pointerEvents = '';
469
+ // If we're waiting for position update, keep fixed position temporarily
470
+ // Otherwise, clear it and let effect handle positioning
471
+ if (!this.waitingForPositionUpdate()) {
472
+ element.style.position = '';
473
+ element.style.left = '';
474
+ element.style.top = '';
475
+ element.style.transform = '';
476
+ }
477
+ // Fallback: if position doesn't update within 500ms, clear waiting state
478
+ // This handles edge cases where the drop was outside the field
479
+ setTimeout(() => {
480
+ if (this.waitingForPositionUpdate()) {
481
+ this.waitingForPositionUpdate.set(false);
482
+ this.lastDragEndPosition.set(null);
483
+ // Clear fixed position and revert to input-based position
484
+ element.style.position = '';
485
+ element.style.left = '';
486
+ element.style.top = '';
487
+ element.style.transform = '';
488
+ }
489
+ }, 500);
356
490
  }
357
491
  onRemoveClick(event) {
358
492
  event.stopPropagation();
359
493
  event.preventDefault();
360
494
  const player = this.player();
361
- console.log('[Player] Remove from field clicked', player.id);
362
495
  this.removeFromField.emit({ playerId: player.id });
363
496
  }
364
497
  onPlayerClick(event) {
365
- // Don't emit click if we're dragging or if the click was on the remove button
366
- if (this.isDragging()) {
498
+ if (this.isDragging() || this.isPotentialDrag()) {
367
499
  return;
368
500
  }
369
501
  event.stopPropagation();
370
502
  const player = this.player();
371
- console.log('[Player] Player clicked', player.id);
372
503
  this.playerClicked.emit({ playerId: player.id, player });
373
504
  }
374
505
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
375
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SoccerBoardPlayerComponent, isStandalone: true, selector: "lib-soccer-board-player", inputs: { player: { classPropertyName: "player", publicName: "player", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dragStart: "dragStart", dragging: "dragging", dragEnd: "dragEnd", removeFromField: "removeFromField", playerClicked: "playerClicked" }, host: { listeners: { "mousedown": "onMouseDown($event)", "touchstart": "onTouchStart($event)", "document:mousemove": "onMouseMove($event)", "document:touchmove": "onTouchMove($event)", "document:mouseup": "onMouseUp($event)", "document:touchend": "onTouchEnd($event)" } }, ngImport: i0, template: "<div\n class=\"player-card group select-none cursor-pointer\"\n [class]=\"sizeClasses()\"\n (click)=\"onPlayerClick($event)\"\n>\n <!-- Golden Border Wrapper -->\n <div class=\"player-border-wrapper\">\n <!-- Main Content Container -->\n <div class=\"player-content relative w-full h-full overflow-hidden rounded-md shadow-lg\">\n <!-- Top Left: Number and Position - Above the photo -->\n <div class=\"player-info-top-left absolute left-0.5 top-1 flex flex-col gap-0.5 z-30\">\n <!-- Player Number -->\n @if (playerNumber()) {\n <div class=\"player-number-badge text-white text-xs font-bold\">\n {{ playerNumber() }}\n </div>\n }\n\n <!-- Position -->\n @if (playerPosition()) {\n <div class=\"player-position-badge text-white text-[10px] font-bold\">\n {{ playerPosition() }}\n </div>\n }\n </div>\n\n <!-- Right Side: Player Photo -->\n <div class=\"player-photo-container absolute right-0 bottom-0 w-4/5 overflow-visible\">\n @if (playerPhoto()) {\n <img\n [src]=\"playerPhoto()!\"\n [alt]=\"playerName()\"\n class=\"player-photo-image\"\n />\n } @else {\n <!-- Placeholder -->\n <div class=\"w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-400 to-gray-600\">\n <svg class=\"w-8 h-8 text-white opacity-50\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fill-rule=\"evenodd\" d=\"M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n }\n </div>\n\n <!-- Hover Effect Overlay -->\n <div class=\"absolute inset-0 bg-white/0 group-hover:bg-white/10 transition-all duration-200 rounded-md pointer-events-none\"></div>\n </div>\n </div>\n\n <!-- Remove Button (only when on field) - Outside the card -->\n @if (isOnField()) {\n <button\n class=\"remove-badge absolute bg-red-500 hover:bg-red-600 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center shadow-lg transition-colors z-30 border-2 border-white\"\n (click)=\"onRemoveClick($event)\"\n type=\"button\"\n aria-label=\"Remove from field\"\n >\n \u00D7\n </button>\n }\n\n <!-- Player Name (outside the card, below) -->\n <div class=\"player-name-outside text-white text-[10px] font-semibold truncate text-center leading-tight mt-0.5\">\n {{ playerName() }}\n </div>\n</div>\n", styles: [":host{display:inline-block;position:relative;cursor:grab;user-select:none;-webkit-user-select:none;touch-action:none}.player-card{position:relative;transition:transform .2s ease,box-shadow .2s ease}.player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.player-card:active{transform:scale(.95)}.player-border-wrapper{position:relative;width:100%;height:100%;padding:2px;border-radius:8px;background:linear-gradient(135deg,#d4af37,#f4d03f,#d4af37,#f4d03f,#d4af37);background-size:200% 200%;animation:shimmer 3s ease-in-out infinite;box-shadow:0 0 8px #d4af3780}.player-content{position:relative;width:100%;height:100%;border-radius:6px;background:repeating-linear-gradient(45deg,#16a34a 0px 2px,#22c55e 2px 4px,#16a34a 4px 6px,#15803d 6px 8px),linear-gradient(135deg,#15803d,#16a34a,#22c55e,#16a34a,#15803d);background-size:20px 20px,100% 100%;background-blend-mode:overlay}@keyframes shimmer{0%,to{background-position:0% 50%}50%{background-position:100% 50%}}.player-info-top-left{z-index:30;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5);background:linear-gradient(to right,rgba(0,0,0,.3) 0%,transparent 50%);padding-right:.5rem;border-radius:0 .25rem .25rem 0}.player-number-badge{font-size:12px;font-weight:900;line-height:1;letter-spacing:-.5px}.player-position-badge{font-size:9px;font-weight:700;line-height:1;letter-spacing:.5px}.remove-badge{box-shadow:0 2px 8px #0006;top:-6px;right:-6px}.player-photo-container{height:100%;width:85%;display:flex;align-items:flex-end;justify-content:flex-start;z-index:1;overflow:visible}.player-photo-image{width:100%;height:auto;max-height:140%;object-fit:contain;object-position:center bottom;display:block}.player-name-outside{width:100%;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5)}:host-context(.w-12) .player-info-top-left{left:.125rem;top:.25rem;gap:.125rem}:host-context(.w-12) .player-number-badge{font-size:9px}:host-context(.w-12) .player-position-badge{font-size:7px}:host-context(.w-12) .player-name-outside{font-size:8px}:host-context(.w-12) .remove-badge{width:18px;height:18px;font-size:10px;top:-2px;right:-2px}:host-context(.w-20) .player-info-top-left{left:.375rem;top:.5rem;gap:.25rem}:host-context(.w-20) .player-number-badge{font-size:16px}:host-context(.w-20) .player-position-badge{font-size:11px}:host-context(.w-20) .player-name-outside{font-size:12px}:host-context(.w-20) .remove-badge{width:24px;height:24px;font-size:14px;top:-3px;right:-3px}\n"] });
506
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SoccerBoardPlayerComponent, isStandalone: true, selector: "lib-soccer-board-player", inputs: { player: { classPropertyName: "player", publicName: "player", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, positionLeft: { classPropertyName: "positionLeft", publicName: "positionLeft", isSignal: true, isRequired: false, transformFunction: null }, positionTop: { classPropertyName: "positionTop", publicName: "positionTop", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dragStart: "dragStart", dragging: "dragging", dragEnd: "dragEnd", removeFromField: "removeFromField", playerClicked: "playerClicked" }, host: { listeners: { "mousedown": "onMouseDown($event)", "touchstart": "onTouchStart($event)", "document:mousemove": "onMouseMove($event)", "document:touchmove": "onTouchMove($event)", "document:mouseup": "onMouseUp($event)", "document:touchend": "onTouchEnd($event)" } }, ngImport: i0, template: "<div\n class=\"player-card group select-none cursor-pointer\"\n [class]=\"sizeClasses()\"\n (click)=\"onPlayerClick($event)\"\n>\n <!-- Golden Border Wrapper -->\n <div class=\"player-border-wrapper\">\n <!-- Main Content Container -->\n <div class=\"player-content relative w-full h-full overflow-hidden rounded-md shadow-lg\">\n <!-- Top Left: Number and Position - Above the photo -->\n <div class=\"player-info-top-left absolute left-0.5 top-1 flex flex-col gap-0.5 z-30\">\n <!-- Player Number -->\n @if (playerNumber()) {\n <div class=\"player-number-badge text-white text-xs font-bold\">\n {{ playerNumber() }}\n </div>\n }\n\n <!-- Position -->\n @if (playerPosition()) {\n <div class=\"player-position-badge text-white text-[10px] font-bold\">\n {{ playerPosition() }}\n </div>\n }\n </div>\n\n <!-- Right Side: Player Photo -->\n <div class=\"player-photo-container absolute right-0 bottom-0 w-4/5 overflow-visible\">\n @if (playerPhoto()) {\n <img\n [src]=\"playerPhoto()!\"\n [alt]=\"playerName()\"\n class=\"player-photo-image\"\n />\n } @else {\n <!-- Placeholder -->\n <div class=\"w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-400 to-gray-600\">\n <svg class=\"w-8 h-8 text-white opacity-50\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fill-rule=\"evenodd\" d=\"M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n }\n </div>\n\n <!-- Hover Effect Overlay -->\n <div class=\"absolute inset-0 bg-white/0 group-hover:bg-white/10 transition-all duration-200 rounded-md pointer-events-none\"></div>\n </div>\n </div>\n\n <!-- Remove Button (only when on field) - Outside the card -->\n @if (isOnField()) {\n <button\n class=\"remove-badge absolute text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center shadow-lg transition-colors z-30 border-2 border-white\"\n (click)=\"onRemoveClick($event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (touchstart)=\"$event.stopPropagation()\"\n type=\"button\"\n aria-label=\"Remove from field\"\n >\n \u00D7\n </button>\n }\n\n <!-- Player Name (outside the card, below) -->\n <div class=\"player-name-outside text-white text-[10px] font-semibold truncate text-center leading-tight mt-0.5\">\n {{ playerName() }}\n </div>\n</div>\n", styles: [":host{display:inline-block;position:relative;cursor:grab;user-select:none;-webkit-user-select:none;touch-action:none}.player-card{position:relative;transition:transform .2s ease,box-shadow .2s ease}.player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.player-card:active{transform:scale(.95)}.player-border-wrapper{position:relative;width:100%;height:100%;padding:2px;border-radius:8px;background:linear-gradient(135deg,#d4af37,#f4d03f,#d4af37,#f4d03f,#d4af37);background-size:200% 200%;animation:shimmer 3s ease-in-out infinite;box-shadow:0 0 8px #d4af3780}.player-content{position:relative;width:100%;height:100%;border-radius:6px;background:repeating-linear-gradient(45deg,#16a34a 0px 2px,#22c55e 2px 4px,#16a34a 4px 6px,#15803d 6px 8px),linear-gradient(135deg,#15803d,#16a34a,#22c55e,#16a34a,#15803d);background-size:20px 20px,100% 100%;background-blend-mode:overlay}@keyframes shimmer{0%,to{background-position:0% 50%}50%{background-position:100% 50%}}.player-info-top-left{z-index:30;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5);background:linear-gradient(to right,rgba(0,0,0,.3) 0%,transparent 50%);padding-right:.5rem;border-radius:0 .25rem .25rem 0}.player-number-badge{font-size:12px;font-weight:900;line-height:1;letter-spacing:-.5px}.player-position-badge{font-size:9px;font-weight:700;line-height:1;letter-spacing:.5px}.remove-badge{background-color:#ef4444!important;color:#fff!important;border-color:#fff!important;box-shadow:0 2px 8px #0006;cursor:pointer!important;top:-6px;right:-6px;pointer-events:auto!important;user-select:none;-webkit-user-select:none}.remove-badge:hover{background-color:#dc2626!important}.remove-badge:active{background-color:#b91c1c!important;transform:scale(.95)}.remove-badge:before,.remove-badge:after{pointer-events:none}.player-photo-container{height:100%;width:85%;display:flex;align-items:flex-end;justify-content:flex-start;z-index:1;overflow:visible}.player-photo-image{width:100%;height:auto;max-height:140%;object-fit:contain;object-position:center bottom;display:block}.player-name-outside{width:100%;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5)}:host-context(.w-12) .player-info-top-left{left:.125rem;top:.25rem;gap:.125rem}:host-context(.w-12) .player-number-badge{font-size:9px}:host-context(.w-12) .player-position-badge{font-size:7px}:host-context(.w-12) .player-name-outside{font-size:8px}:host-context(.w-12) .remove-badge{width:18px;height:18px;font-size:10px;top:-2px;right:-2px}:host-context(.w-20) .player-info-top-left{left:.375rem;top:.5rem;gap:.25rem}:host-context(.w-20) .player-number-badge{font-size:16px}:host-context(.w-20) .player-position-badge{font-size:11px}:host-context(.w-20) .player-name-outside{font-size:12px}:host-context(.w-20) .remove-badge{width:24px;height:24px;font-size:14px;top:-3px;right:-3px}\n"] });
376
507
  }
377
508
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardPlayerComponent, decorators: [{
378
509
  type: Component,
379
- args: [{ selector: 'lib-soccer-board-player', standalone: true, imports: [], template: "<div\n class=\"player-card group select-none cursor-pointer\"\n [class]=\"sizeClasses()\"\n (click)=\"onPlayerClick($event)\"\n>\n <!-- Golden Border Wrapper -->\n <div class=\"player-border-wrapper\">\n <!-- Main Content Container -->\n <div class=\"player-content relative w-full h-full overflow-hidden rounded-md shadow-lg\">\n <!-- Top Left: Number and Position - Above the photo -->\n <div class=\"player-info-top-left absolute left-0.5 top-1 flex flex-col gap-0.5 z-30\">\n <!-- Player Number -->\n @if (playerNumber()) {\n <div class=\"player-number-badge text-white text-xs font-bold\">\n {{ playerNumber() }}\n </div>\n }\n\n <!-- Position -->\n @if (playerPosition()) {\n <div class=\"player-position-badge text-white text-[10px] font-bold\">\n {{ playerPosition() }}\n </div>\n }\n </div>\n\n <!-- Right Side: Player Photo -->\n <div class=\"player-photo-container absolute right-0 bottom-0 w-4/5 overflow-visible\">\n @if (playerPhoto()) {\n <img\n [src]=\"playerPhoto()!\"\n [alt]=\"playerName()\"\n class=\"player-photo-image\"\n />\n } @else {\n <!-- Placeholder -->\n <div class=\"w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-400 to-gray-600\">\n <svg class=\"w-8 h-8 text-white opacity-50\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fill-rule=\"evenodd\" d=\"M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n }\n </div>\n\n <!-- Hover Effect Overlay -->\n <div class=\"absolute inset-0 bg-white/0 group-hover:bg-white/10 transition-all duration-200 rounded-md pointer-events-none\"></div>\n </div>\n </div>\n\n <!-- Remove Button (only when on field) - Outside the card -->\n @if (isOnField()) {\n <button\n class=\"remove-badge absolute bg-red-500 hover:bg-red-600 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center shadow-lg transition-colors z-30 border-2 border-white\"\n (click)=\"onRemoveClick($event)\"\n type=\"button\"\n aria-label=\"Remove from field\"\n >\n \u00D7\n </button>\n }\n\n <!-- Player Name (outside the card, below) -->\n <div class=\"player-name-outside text-white text-[10px] font-semibold truncate text-center leading-tight mt-0.5\">\n {{ playerName() }}\n </div>\n</div>\n", styles: [":host{display:inline-block;position:relative;cursor:grab;user-select:none;-webkit-user-select:none;touch-action:none}.player-card{position:relative;transition:transform .2s ease,box-shadow .2s ease}.player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.player-card:active{transform:scale(.95)}.player-border-wrapper{position:relative;width:100%;height:100%;padding:2px;border-radius:8px;background:linear-gradient(135deg,#d4af37,#f4d03f,#d4af37,#f4d03f,#d4af37);background-size:200% 200%;animation:shimmer 3s ease-in-out infinite;box-shadow:0 0 8px #d4af3780}.player-content{position:relative;width:100%;height:100%;border-radius:6px;background:repeating-linear-gradient(45deg,#16a34a 0px 2px,#22c55e 2px 4px,#16a34a 4px 6px,#15803d 6px 8px),linear-gradient(135deg,#15803d,#16a34a,#22c55e,#16a34a,#15803d);background-size:20px 20px,100% 100%;background-blend-mode:overlay}@keyframes shimmer{0%,to{background-position:0% 50%}50%{background-position:100% 50%}}.player-info-top-left{z-index:30;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5);background:linear-gradient(to right,rgba(0,0,0,.3) 0%,transparent 50%);padding-right:.5rem;border-radius:0 .25rem .25rem 0}.player-number-badge{font-size:12px;font-weight:900;line-height:1;letter-spacing:-.5px}.player-position-badge{font-size:9px;font-weight:700;line-height:1;letter-spacing:.5px}.remove-badge{box-shadow:0 2px 8px #0006;top:-6px;right:-6px}.player-photo-container{height:100%;width:85%;display:flex;align-items:flex-end;justify-content:flex-start;z-index:1;overflow:visible}.player-photo-image{width:100%;height:auto;max-height:140%;object-fit:contain;object-position:center bottom;display:block}.player-name-outside{width:100%;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5)}:host-context(.w-12) .player-info-top-left{left:.125rem;top:.25rem;gap:.125rem}:host-context(.w-12) .player-number-badge{font-size:9px}:host-context(.w-12) .player-position-badge{font-size:7px}:host-context(.w-12) .player-name-outside{font-size:8px}:host-context(.w-12) .remove-badge{width:18px;height:18px;font-size:10px;top:-2px;right:-2px}:host-context(.w-20) .player-info-top-left{left:.375rem;top:.5rem;gap:.25rem}:host-context(.w-20) .player-number-badge{font-size:16px}:host-context(.w-20) .player-position-badge{font-size:11px}:host-context(.w-20) .player-name-outside{font-size:12px}:host-context(.w-20) .remove-badge{width:24px;height:24px;font-size:14px;top:-3px;right:-3px}\n"] }]
380
- }], ctorParameters: () => [], propDecorators: { player: [{ type: i0.Input, args: [{ isSignal: true, alias: "player", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragging: [{ type: i0.Output, args: ["dragging"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }], removeFromField: [{ type: i0.Output, args: ["removeFromField"] }], playerClicked: [{ type: i0.Output, args: ["playerClicked"] }], onMouseDown: [{
510
+ args: [{ selector: 'lib-soccer-board-player', standalone: true, imports: [], template: "<div\n class=\"player-card group select-none cursor-pointer\"\n [class]=\"sizeClasses()\"\n (click)=\"onPlayerClick($event)\"\n>\n <!-- Golden Border Wrapper -->\n <div class=\"player-border-wrapper\">\n <!-- Main Content Container -->\n <div class=\"player-content relative w-full h-full overflow-hidden rounded-md shadow-lg\">\n <!-- Top Left: Number and Position - Above the photo -->\n <div class=\"player-info-top-left absolute left-0.5 top-1 flex flex-col gap-0.5 z-30\">\n <!-- Player Number -->\n @if (playerNumber()) {\n <div class=\"player-number-badge text-white text-xs font-bold\">\n {{ playerNumber() }}\n </div>\n }\n\n <!-- Position -->\n @if (playerPosition()) {\n <div class=\"player-position-badge text-white text-[10px] font-bold\">\n {{ playerPosition() }}\n </div>\n }\n </div>\n\n <!-- Right Side: Player Photo -->\n <div class=\"player-photo-container absolute right-0 bottom-0 w-4/5 overflow-visible\">\n @if (playerPhoto()) {\n <img\n [src]=\"playerPhoto()!\"\n [alt]=\"playerName()\"\n class=\"player-photo-image\"\n />\n } @else {\n <!-- Placeholder -->\n <div class=\"w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-400 to-gray-600\">\n <svg class=\"w-8 h-8 text-white opacity-50\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fill-rule=\"evenodd\" d=\"M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n }\n </div>\n\n <!-- Hover Effect Overlay -->\n <div class=\"absolute inset-0 bg-white/0 group-hover:bg-white/10 transition-all duration-200 rounded-md pointer-events-none\"></div>\n </div>\n </div>\n\n <!-- Remove Button (only when on field) - Outside the card -->\n @if (isOnField()) {\n <button\n class=\"remove-badge absolute text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center shadow-lg transition-colors z-30 border-2 border-white\"\n (click)=\"onRemoveClick($event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (touchstart)=\"$event.stopPropagation()\"\n type=\"button\"\n aria-label=\"Remove from field\"\n >\n \u00D7\n </button>\n }\n\n <!-- Player Name (outside the card, below) -->\n <div class=\"player-name-outside text-white text-[10px] font-semibold truncate text-center leading-tight mt-0.5\">\n {{ playerName() }}\n </div>\n</div>\n", styles: [":host{display:inline-block;position:relative;cursor:grab;user-select:none;-webkit-user-select:none;touch-action:none}.player-card{position:relative;transition:transform .2s ease,box-shadow .2s ease}.player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.player-card:active{transform:scale(.95)}.player-border-wrapper{position:relative;width:100%;height:100%;padding:2px;border-radius:8px;background:linear-gradient(135deg,#d4af37,#f4d03f,#d4af37,#f4d03f,#d4af37);background-size:200% 200%;animation:shimmer 3s ease-in-out infinite;box-shadow:0 0 8px #d4af3780}.player-content{position:relative;width:100%;height:100%;border-radius:6px;background:repeating-linear-gradient(45deg,#16a34a 0px 2px,#22c55e 2px 4px,#16a34a 4px 6px,#15803d 6px 8px),linear-gradient(135deg,#15803d,#16a34a,#22c55e,#16a34a,#15803d);background-size:20px 20px,100% 100%;background-blend-mode:overlay}@keyframes shimmer{0%,to{background-position:0% 50%}50%{background-position:100% 50%}}.player-info-top-left{z-index:30;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5);background:linear-gradient(to right,rgba(0,0,0,.3) 0%,transparent 50%);padding-right:.5rem;border-radius:0 .25rem .25rem 0}.player-number-badge{font-size:12px;font-weight:900;line-height:1;letter-spacing:-.5px}.player-position-badge{font-size:9px;font-weight:700;line-height:1;letter-spacing:.5px}.remove-badge{background-color:#ef4444!important;color:#fff!important;border-color:#fff!important;box-shadow:0 2px 8px #0006;cursor:pointer!important;top:-6px;right:-6px;pointer-events:auto!important;user-select:none;-webkit-user-select:none}.remove-badge:hover{background-color:#dc2626!important}.remove-badge:active{background-color:#b91c1c!important;transform:scale(.95)}.remove-badge:before,.remove-badge:after{pointer-events:none}.player-photo-container{height:100%;width:85%;display:flex;align-items:flex-end;justify-content:flex-start;z-index:1;overflow:visible}.player-photo-image{width:100%;height:auto;max-height:140%;object-fit:contain;object-position:center bottom;display:block}.player-name-outside{width:100%;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5)}:host-context(.w-12) .player-info-top-left{left:.125rem;top:.25rem;gap:.125rem}:host-context(.w-12) .player-number-badge{font-size:9px}:host-context(.w-12) .player-position-badge{font-size:7px}:host-context(.w-12) .player-name-outside{font-size:8px}:host-context(.w-12) .remove-badge{width:18px;height:18px;font-size:10px;top:-2px;right:-2px}:host-context(.w-20) .player-info-top-left{left:.375rem;top:.5rem;gap:.25rem}:host-context(.w-20) .player-number-badge{font-size:16px}:host-context(.w-20) .player-position-badge{font-size:11px}:host-context(.w-20) .player-name-outside{font-size:12px}:host-context(.w-20) .remove-badge{width:24px;height:24px;font-size:14px;top:-3px;right:-3px}\n"] }]
511
+ }], ctorParameters: () => [], propDecorators: { player: [{ type: i0.Input, args: [{ isSignal: true, alias: "player", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], positionLeft: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionLeft", required: false }] }], positionTop: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionTop", required: false }] }], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragging: [{ type: i0.Output, args: ["dragging"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }], removeFromField: [{ type: i0.Output, args: ["removeFromField"] }], playerClicked: [{ type: i0.Output, args: ["playerClicked"] }], onMouseDown: [{
381
512
  type: HostListener,
382
513
  args: ['mousedown', ['$event']]
383
514
  }], onTouchStart: [{
@@ -430,6 +561,12 @@ class SoccerBoardComponent {
430
561
  playerRemovedFromField = output();
431
562
  imageExported = output();
432
563
  playerClicked = output();
564
+ // Method to handle player drag start (called from template)
565
+ onPlayerDragStart(event) {
566
+ // Player component handles its own drag positioning
567
+ // This is just for logging/debugging
568
+ console.log('[SoccerBoard] Drag started', event.playerId);
569
+ }
433
570
  // Method to handle player drag end (called from template)
434
571
  onPlayerDragEnd(event) {
435
572
  this.handlePlayerDrop(event);
@@ -503,11 +640,22 @@ class SoccerBoardComponent {
503
640
  });
504
641
  }, ...(ngDevMode ? [{ debugName: "awayZones" }] : []));
505
642
  // Computed players by side
643
+ // These computed signals automatically cache results when dependencies haven't changed
644
+ // The @for loop uses trackBy with player.id to only re-render changed players
645
+ // NOTE: Even if a new array is returned, trackBy ensures only changed items re-render
506
646
  homePlayers = computed(() => {
507
- return this.players().filter((player) => player.side === SoccerBoardTeamSide.Home && player.fieldX !== undefined && player.fieldY !== undefined);
647
+ return this.players().filter((player) => player.side === SoccerBoardTeamSide.Home &&
648
+ player.fieldX !== undefined &&
649
+ player.fieldY !== undefined &&
650
+ !isNaN(player.fieldX) &&
651
+ !isNaN(player.fieldY));
508
652
  }, ...(ngDevMode ? [{ debugName: "homePlayers" }] : []));
509
653
  awayPlayers = computed(() => {
510
- return this.players().filter((player) => player.side === SoccerBoardTeamSide.Away && player.fieldX !== undefined && player.fieldY !== undefined);
654
+ return this.players().filter((player) => player.side === SoccerBoardTeamSide.Away &&
655
+ player.fieldX !== undefined &&
656
+ player.fieldY !== undefined &&
657
+ !isNaN(player.fieldX) &&
658
+ !isNaN(player.fieldY));
511
659
  }, ...(ngDevMode ? [{ debugName: "awayPlayers" }] : []));
512
660
  // Computed: players available (not on field)
513
661
  availablePlayers = computed(() => {
@@ -534,12 +682,51 @@ class SoccerBoardComponent {
534
682
  playersGridColumns = computed(() => {
535
683
  return `repeat(${this.playersColumns()}, minmax(0, 1fr))`;
536
684
  }, ...(ngDevMode ? [{ debugName: "playersGridColumns" }] : []));
685
+ // Computed: Map of player positions for stable rendering
686
+ // This ensures positions are only recalculated when players() or orientation() change
687
+ // Using a Map allows efficient lookups
688
+ playerPositions = computed(() => {
689
+ const positions = new Map();
690
+ const players = this.players();
691
+ // Track orientation dependency - fieldToHalfCoordinates uses it internally
692
+ this.orientation();
693
+ // Process all players with valid coordinates
694
+ for (const player of players) {
695
+ if (player.fieldX !== undefined &&
696
+ player.fieldY !== undefined &&
697
+ player.side !== undefined &&
698
+ !isNaN(player.fieldX) &&
699
+ !isNaN(player.fieldY)) {
700
+ // Validate coordinates are within bounds (0-100)
701
+ const fieldX = Math.max(0, Math.min(100, player.fieldX));
702
+ const fieldY = Math.max(0, Math.min(100, player.fieldY));
703
+ // Calculate position using validated coordinates
704
+ const position = this.fieldToHalfCoordinates(fieldX, fieldY, player.side);
705
+ positions.set(player.id, position);
706
+ }
707
+ }
708
+ return positions;
709
+ }, ...(ngDevMode ? [{ debugName: "playerPositions" }] : []));
537
710
  // Helper method to get visual position for a player (public for template)
711
+ // Now uses the computed positions map instead of calculating on the fly
538
712
  getPlayerPosition(player) {
539
- if (player.fieldX === undefined || player.fieldY === undefined || !player.side) {
540
- return { left: 0, top: 0 };
713
+ const positions = this.playerPositions();
714
+ const position = positions.get(player.id);
715
+ if (position) {
716
+ return position;
541
717
  }
542
- return this.fieldToHalfCoordinates(player.fieldX, player.fieldY, player.side);
718
+ // Fallback: if position is not in cache but player has coordinates, calculate it
719
+ // This should rarely happen, but handles edge cases
720
+ if (player.fieldX !== undefined &&
721
+ player.fieldY !== undefined &&
722
+ player.side !== undefined &&
723
+ !isNaN(player.fieldX) &&
724
+ !isNaN(player.fieldY)) {
725
+ const fieldX = Math.max(0, Math.min(100, player.fieldX));
726
+ const fieldY = Math.max(0, Math.min(100, player.fieldY));
727
+ return this.fieldToHalfCoordinates(fieldX, fieldY, player.side);
728
+ }
729
+ return { left: 0, top: 0 };
543
730
  }
544
731
  resizeObserver = null;
545
732
  constructor() {
@@ -572,57 +759,106 @@ class SoccerBoardComponent {
572
759
  */
573
760
  handlePlayerDrop(event) {
574
761
  console.log('[SoccerBoard] handlePlayerDrop called', event);
575
- const position = this.calculateFieldPosition(event.clientX, event.clientY);
762
+ // Calculate position immediately (don't use requestAnimationFrame as it causes timing issues)
763
+ // The coordinates are already correct from the drag end event
764
+ // clientX/clientY represent where the mouse/touch was when released
765
+ // offsetX/offsetY represent the offset from element's top-left to click point
766
+ // elementWidth/elementHeight are the dimensions of the dragged element
767
+ const position = this.calculateFieldPosition(event.clientX, event.clientY, event.offsetX, event.offsetY, event.elementWidth, event.elementHeight);
576
768
  console.log('[SoccerBoard] Calculated position', position);
577
- if (position) {
578
- // Check if player is already on the field (moving to a different position)
579
- const currentPlayer = this.players().find((p) => p.id === event.playerId);
580
- const isMovingPlayer = currentPlayer?.fieldX !== undefined && currentPlayer?.fieldY !== undefined;
581
- // Check maximum players per side (only for new players, not when moving)
582
- if (!isMovingPlayer) {
583
- const playersOnSide = this.players().filter((p) => p.side === position.side && p.fieldX !== undefined && p.fieldY !== undefined).length;
584
- if (playersOnSide >= this.maxPlayersPerSide()) {
585
- console.warn(`[SoccerBoard] Maximum players (${this.maxPlayersPerSide()}) reached for ${position.side} side`);
586
- return; // Don't allow drop if max players reached
587
- }
588
- }
589
- const tacticalPosition = this.detectTacticalPosition(position.fieldX, position.fieldY, position.side);
590
- console.log('🎯 Player dropped:', {
591
- playerId: event.playerId,
592
- position: tacticalPosition,
593
- coordinates: {
594
- fieldX: position.fieldX,
595
- fieldY: position.fieldY,
596
- side: position.side,
597
- },
598
- });
599
- // Emit event with position information
600
- this.playerPositioned.emit({
601
- playerId: event.playerId,
602
- fieldX: position.fieldX,
603
- fieldY: position.fieldY,
604
- side: position.side,
605
- position: tacticalPosition,
606
- });
607
- }
608
- else {
769
+ if (!position) {
609
770
  console.warn('[SoccerBoard] Position is null - player not dropped on field');
771
+ return;
610
772
  }
773
+ // Get current player state BEFORE validation to avoid race conditions
774
+ // Use the current players() signal value to get the latest state
775
+ const currentPlayers = this.players();
776
+ const currentPlayer = currentPlayers.find((p) => p.id === event.playerId);
777
+ if (!currentPlayer) {
778
+ console.warn('[SoccerBoard] Player not found', event.playerId);
779
+ return;
780
+ }
781
+ const isMovingPlayer = currentPlayer.fieldX !== undefined && currentPlayer.fieldY !== undefined;
782
+ const isMovingToDifferentSide = isMovingPlayer && currentPlayer.side !== position.side;
783
+ // Check maximum players per side:
784
+ // - For new players: always check
785
+ // - For moving players: only check if moving to a DIFFERENT side
786
+ // - When moving within the same side, allow it (no limit check needed)
787
+ if (!isMovingPlayer || isMovingToDifferentSide) {
788
+ // Count players on the target side (excluding the current player if moving)
789
+ const playersOnTargetSide = currentPlayers.filter((p) => p.id !== event.playerId && // Exclude the player being moved
790
+ p.side === position.side &&
791
+ p.fieldX !== undefined &&
792
+ p.fieldY !== undefined &&
793
+ !isNaN(p.fieldX) &&
794
+ !isNaN(p.fieldY)).length;
795
+ if (playersOnTargetSide >= this.maxPlayersPerSide()) {
796
+ console.warn(`[SoccerBoard] Maximum players (${this.maxPlayersPerSide()}) reached for ${position.side} side. Cannot ${isMovingPlayer ? 'move' : 'add'} player.`);
797
+ return; // Don't allow drop if max players reached on target side
798
+ }
799
+ }
800
+ // Validate coordinates are within bounds
801
+ const fieldX = Math.max(0, Math.min(100, position.fieldX));
802
+ const fieldY = Math.max(0, Math.min(100, position.fieldY));
803
+ const tacticalPosition = this.detectTacticalPosition(fieldX, fieldY, position.side);
804
+ console.log('🎯 Player dropped:', {
805
+ playerId: event.playerId,
806
+ isMovingPlayer,
807
+ isMovingToDifferentSide,
808
+ position: tacticalPosition,
809
+ coordinates: {
810
+ fieldX,
811
+ fieldY,
812
+ side: position.side,
813
+ },
814
+ });
815
+ // Emit event with position information (using validated coordinates)
816
+ // Emit immediately - the parent component handles the update
817
+ this.playerPositioned.emit({
818
+ playerId: event.playerId,
819
+ fieldX,
820
+ fieldY,
821
+ side: position.side,
822
+ position: tacticalPosition,
823
+ });
611
824
  }
612
825
  /**
613
826
  * Calculates field position from client coordinates
827
+ * @param clientX - X coordinate of the mouse/touch event
828
+ * @param clientY - Y coordinate of the mouse/touch event
829
+ * @param offsetX - Optional: X offset from element's left edge to click point (used to calculate center)
830
+ * @param offsetY - Optional: Y offset from element's top edge to click point (used to calculate center)
831
+ * @param elementWidth - Optional: Width of the dragged element
832
+ * @param elementHeight - Optional: Height of the dragged element
614
833
  */
615
- calculateFieldPosition(clientX, clientY) {
834
+ calculateFieldPosition(clientX, clientY, offsetX, offsetY, elementWidth, elementHeight) {
616
835
  const homeHalfEl = this.homeHalf()?.nativeElement;
617
836
  const awayHalfEl = this.awayHalf()?.nativeElement;
618
837
  if (!homeHalfEl || !awayHalfEl) {
619
838
  return null;
620
839
  }
621
- // Get the center point of the dragged element
622
- // Use clientX/clientY directly as they represent where the mouse/touch is
623
- // The offset is already accounted for in the drag handling
624
- const centerX = clientX;
625
- const centerY = clientY;
840
+ // Calculate the center of the dragged element
841
+ // clientX/clientY is where the cursor is
842
+ // offsetX/offsetY is the offset from the element's top-left to where the user clicked
843
+ // elementWidth/elementHeight are the actual dimensions of the element
844
+ //
845
+ // The element's top-left corner is at:
846
+ // elementLeft = clientX - offsetX
847
+ // elementTop = clientY - offsetY
848
+ //
849
+ // The element's center is at:
850
+ // centerX = elementLeft + elementWidth / 2 = clientX - offsetX + elementWidth / 2
851
+ // centerY = elementTop + elementHeight / 2 = clientY - offsetY + elementHeight / 2
852
+ let centerX = clientX;
853
+ let centerY = clientY;
854
+ if (offsetX !== undefined &&
855
+ offsetY !== undefined &&
856
+ elementWidth !== undefined &&
857
+ elementHeight !== undefined) {
858
+ // Calculate the actual center of the element
859
+ centerX = clientX - offsetX + elementWidth / 2;
860
+ centerY = clientY - offsetY + elementHeight / 2;
861
+ }
626
862
  // Check HOME half
627
863
  const homeRect = homeHalfEl.getBoundingClientRect();
628
864
  const isInHome = centerX >= homeRect.left &&
@@ -937,85 +1173,19 @@ class SoccerBoardComponent {
937
1173
  }
938
1174
  }
939
1175
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
940
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SoccerBoardComponent, isStandalone: true, selector: "fuzo-soccer-board", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, teamSide: { classPropertyName: "teamSide", publicName: "teamSide", isSignal: true, isRequired: false, transformFunction: null }, showPositions: { classPropertyName: "showPositions", publicName: "showPositions", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null }, players: { classPropertyName: "players", publicName: "players", isSignal: true, isRequired: false, transformFunction: null }, showPlayersSidebar: { classPropertyName: "showPlayersSidebar", publicName: "showPlayersSidebar", isSignal: true, isRequired: false, transformFunction: null }, playersPosition: { classPropertyName: "playersPosition", publicName: "playersPosition", isSignal: true, isRequired: false, transformFunction: null }, playersColumns: { classPropertyName: "playersColumns", publicName: "playersColumns", isSignal: true, isRequired: false, transformFunction: null }, maxPlayersPerSide: { classPropertyName: "maxPlayersPerSide", publicName: "maxPlayersPerSide", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playerPositioned: "playerPositioned", playerRemovedFromField: "playerRemovedFromField", imageExported: "imageExported", playerClicked: "playerClicked" }, host: { properties: { "style.width": "this.hostWidthStyle" } }, viewQueries: [{ propertyName: "fieldContainer", first: true, predicate: ["fieldContainer"], descendants: true, isSignal: true }, { propertyName: "homeHalf", first: true, predicate: ["homeHalf"], descendants: true, isSignal: true }, { propertyName: "awayHalf", first: true, predicate: ["awayHalf"], descendants: true, isSignal: true }, { propertyName: "soccerBoardContainer", first: true, predicate: ["soccerBoardContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"soccer-board-wrapper\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n @if (showPlayersSidebar()) {\n <div class=\"players-sidebar\">\n <div\n class=\"players-grid\"\n [style.grid-template-columns]=\"playersGridColumns()\"\n >\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Soccer Board Container -->\n <div #soccerBoardContainer class=\"soccer-board-container\">\n <div\n #fieldContainer\n class=\"field-container\"\n [class.landscape]=\"isLandscape()\"\n >\n <!-- AWAY half -->\n <div #awayHalf class=\"field-half away\" [class.hidden]=\"!showAway()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of awayZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-away\">\n @for (player of awayPlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n\n <!-- HOME half -->\n <div #homeHalf class=\"field-half home\" [class.hidden]=\"!showHome()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of homeZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-home\">\n @for (player of homePlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;height:100%}*{box-sizing:border-box}.soccer-board-wrapper{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .soccer-board-container{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .soccer-board-container{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .soccer-board-container{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .soccer-board-container{flex:1;min-height:0}.layout-no-sidebar .soccer-board-container{width:100%;height:100%}.players-sidebar{display:flex;flex-direction:column;background:transparent;padding:0;overflow-y:auto}.players-grid{display:grid;gap:.5rem;align-content:start}.soccer-board-container{width:100%;display:flex;justify-content:center;align-items:center;padding:var(--soccer-board-padding, 0);max-width:var(--soccer-board-max-width, none);width:var(--soccer-board-width, 100%);aspect-ratio:105/68}.field-container{display:flex;flex-direction:column;gap:0;width:100%;min-width:300px;height:auto}.field-container.landscape{flex-direction:row}.field-container.landscape .field-half{width:50%;aspect-ratio:52.5/68;background:repeating-linear-gradient(90deg,#2d5016 0px 10px,#3a6b1f 10px 20px)}.field-container.landscape .field-half.home{border-radius:0 10px 10px 0;box-shadow:10px 0 30px #0000004d}.field-container.landscape .field-half.away{border-radius:10px 0 0 10px;box-shadow:-5px 0 20px #0003}.field-container.landscape .field-half.home .center-border{inset:0 auto 0 0;width:3px;height:auto}.field-container.landscape .field-half.away .center-border{inset:0 0 0 auto;width:3px;height:auto}.field-container.landscape .field-half.home .half-circle{top:50%;left:0;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-left:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:0 999px 999px 0}.field-container.landscape .field-half.away .half-circle{inset:50% 0 auto auto;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-right:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:999px 0 0 999px}.field-container.landscape .field-half.home .penalty-area{inset:50% 0 auto auto;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .penalty-area{inset:50% auto auto 0;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-container.landscape .field-half.home .goal-area{inset:50% 0 auto auto;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .goal-area{inset:50% auto auto 0;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-half{position:relative;width:100%;aspect-ratio:68/52.5;background:repeating-linear-gradient(0deg,#2d5016 0px 10px,#3a6b1f 10px 20px);overflow:hidden}.field-half.home{border-radius:0 0 10px 10px;box-shadow:0 10px 30px #0000004d}.field-half.away{border-radius:10px 10px 0 0;box-shadow:0 -5px 20px #0003}.field-half.hidden{display:none}.field-lines{position:absolute;inset:0;pointer-events:none}.field-half.home .center-border{position:absolute;background:#fffc;z-index:10;top:0;left:0;right:0;height:3px}.field-half.away .center-border{position:absolute;background:#fffc;z-index:10;bottom:0;left:0;right:0;height:3px}.field-half.home .half-circle{position:absolute;z-index:5;top:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-top:none;border-radius:0 0 999px 999px;background:transparent}.field-half.away .half-circle{position:absolute;z-index:5;bottom:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-bottom:none;border-radius:999px 999px 0 0;background:transparent}.field-half.home .penalty-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .penalty-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-top:none}.field-half.home .goal-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .goal-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-top:none}.position-zones{position:absolute;inset:0;pointer-events:none;z-index:15}.position-zones.hidden{display:none}.position-zone{position:absolute;border:2px solid rgba(255,255,255,.8);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.8);opacity:.85;transition:all .3s ease}.position-zone:hover{opacity:1;z-index:100;transform:scale(1.05)}.players-container{position:absolute;inset:0;pointer-events:none;z-index:20}.players-container>*{pointer-events:auto}\n"], dependencies: [{ kind: "component", type: SoccerBoardPlayerComponent, selector: "lib-soccer-board-player", inputs: ["player", "size"], outputs: ["dragStart", "dragging", "dragEnd", "removeFromField", "playerClicked"] }] });
1176
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SoccerBoardComponent, isStandalone: true, selector: "fuzo-soccer-board", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, teamSide: { classPropertyName: "teamSide", publicName: "teamSide", isSignal: true, isRequired: false, transformFunction: null }, showPositions: { classPropertyName: "showPositions", publicName: "showPositions", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null }, players: { classPropertyName: "players", publicName: "players", isSignal: true, isRequired: false, transformFunction: null }, showPlayersSidebar: { classPropertyName: "showPlayersSidebar", publicName: "showPlayersSidebar", isSignal: true, isRequired: false, transformFunction: null }, playersPosition: { classPropertyName: "playersPosition", publicName: "playersPosition", isSignal: true, isRequired: false, transformFunction: null }, playersColumns: { classPropertyName: "playersColumns", publicName: "playersColumns", isSignal: true, isRequired: false, transformFunction: null }, maxPlayersPerSide: { classPropertyName: "maxPlayersPerSide", publicName: "maxPlayersPerSide", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playerPositioned: "playerPositioned", playerRemovedFromField: "playerRemovedFromField", imageExported: "imageExported", playerClicked: "playerClicked" }, host: { properties: { "style.width": "this.hostWidthStyle" } }, viewQueries: [{ propertyName: "fieldContainer", first: true, predicate: ["fieldContainer"], descendants: true, isSignal: true }, { propertyName: "homeHalf", first: true, predicate: ["homeHalf"], descendants: true, isSignal: true }, { propertyName: "awayHalf", first: true, predicate: ["awayHalf"], descendants: true, isSignal: true }, { propertyName: "soccerBoardContainer", first: true, predicate: ["soccerBoardContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"soccer-board-wrapper\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n @if (showPlayersSidebar()) {\n <div class=\"players-sidebar\">\n <div\n class=\"players-grid\"\n [style.grid-template-columns]=\"playersGridColumns()\"\n >\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragStart)=\"onPlayerDragStart($event)\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Soccer Board Container -->\n <div #soccerBoardContainer class=\"soccer-board-container\">\n <div\n #fieldContainer\n class=\"field-container\"\n [class.landscape]=\"isLandscape()\"\n >\n <!-- AWAY half -->\n <div #awayHalf class=\"field-half away\" [class.hidden]=\"!showAway()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of awayZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-away\">\n @for (player of awayPlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [positionLeft]=\"getPlayerPosition(player).left\"\n [positionTop]=\"getPlayerPosition(player).top\"\n (dragStart)=\"onPlayerDragStart($event)\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n\n <!-- HOME half -->\n <div #homeHalf class=\"field-half home\" [class.hidden]=\"!showHome()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of homeZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-home\">\n @for (player of homePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [positionLeft]=\"getPlayerPosition(player).left\"\n [positionTop]=\"getPlayerPosition(player).top\"\n (dragStart)=\"onPlayerDragStart($event)\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;height:100%}*{box-sizing:border-box}.soccer-board-wrapper{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .soccer-board-container{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .soccer-board-container{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .soccer-board-container{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .soccer-board-container{flex:1;min-height:0}.layout-no-sidebar .soccer-board-container{width:100%;height:100%}.players-sidebar{display:flex;flex-direction:column;background:transparent;padding:0;overflow-y:auto}.players-grid{display:grid;gap:.5rem;align-content:start}.soccer-board-container{width:100%;display:flex;justify-content:center;align-items:center;padding:var(--soccer-board-padding, 0);max-width:var(--soccer-board-max-width, none);width:var(--soccer-board-width, 100%);aspect-ratio:105/68}.field-container{display:flex;flex-direction:column;gap:0;width:100%;min-width:300px;height:auto}.field-container.landscape{flex-direction:row}.field-container.landscape .field-half{width:50%;aspect-ratio:52.5/68;background:repeating-linear-gradient(90deg,#2d5016 0px 10px,#3a6b1f 10px 20px)}.field-container.landscape .field-half.home{border-radius:0 10px 10px 0;box-shadow:10px 0 30px #0000004d}.field-container.landscape .field-half.away{border-radius:10px 0 0 10px;box-shadow:-5px 0 20px #0003}.field-container.landscape .field-half.home .center-border{inset:0 auto 0 0;width:3px;height:auto}.field-container.landscape .field-half.away .center-border{inset:0 0 0 auto;width:3px;height:auto}.field-container.landscape .field-half.home .half-circle{top:50%;left:0;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-left:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:0 999px 999px 0}.field-container.landscape .field-half.away .half-circle{inset:50% 0 auto auto;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-right:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:999px 0 0 999px}.field-container.landscape .field-half.home .penalty-area{inset:50% 0 auto auto;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .penalty-area{inset:50% auto auto 0;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-container.landscape .field-half.home .goal-area{inset:50% 0 auto auto;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .goal-area{inset:50% auto auto 0;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-half{position:relative;width:100%;aspect-ratio:68/52.5;background:repeating-linear-gradient(0deg,#2d5016 0px 10px,#3a6b1f 10px 20px);overflow:hidden}.field-half.home{border-radius:0 0 10px 10px;box-shadow:0 10px 30px #0000004d}.field-half.away{border-radius:10px 10px 0 0;box-shadow:0 -5px 20px #0003}.field-half.hidden{display:none}.field-lines{position:absolute;inset:0;pointer-events:none}.field-half.home .center-border{position:absolute;background:#fffc;z-index:10;top:0;left:0;right:0;height:3px}.field-half.away .center-border{position:absolute;background:#fffc;z-index:10;bottom:0;left:0;right:0;height:3px}.field-half.home .half-circle{position:absolute;z-index:5;top:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-top:none;border-radius:0 0 999px 999px;background:transparent}.field-half.away .half-circle{position:absolute;z-index:5;bottom:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-bottom:none;border-radius:999px 999px 0 0;background:transparent}.field-half.home .penalty-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .penalty-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-top:none}.field-half.home .goal-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .goal-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-top:none}.position-zones{position:absolute;inset:0;pointer-events:none;z-index:15}.position-zones.hidden{display:none}.position-zone{position:absolute;border:2px solid rgba(255,255,255,.8);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.8);opacity:.85;transition:all .3s ease}.position-zone:hover{opacity:1;z-index:100;transform:scale(1.05)}.players-container{position:absolute;inset:0;pointer-events:none;z-index:20}.players-container>*{pointer-events:auto}\n"], dependencies: [{ kind: "component", type: SoccerBoardPlayerComponent, selector: "lib-soccer-board-player", inputs: ["player", "size", "positionLeft", "positionTop"], outputs: ["dragStart", "dragging", "dragEnd", "removeFromField", "playerClicked"] }] });
941
1177
  }
942
1178
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardComponent, decorators: [{
943
1179
  type: Component,
944
- args: [{ selector: 'fuzo-soccer-board', standalone: true, imports: [SoccerBoardPlayerComponent], template: "<div class=\"soccer-board-wrapper\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n @if (showPlayersSidebar()) {\n <div class=\"players-sidebar\">\n <div\n class=\"players-grid\"\n [style.grid-template-columns]=\"playersGridColumns()\"\n >\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Soccer Board Container -->\n <div #soccerBoardContainer class=\"soccer-board-container\">\n <div\n #fieldContainer\n class=\"field-container\"\n [class.landscape]=\"isLandscape()\"\n >\n <!-- AWAY half -->\n <div #awayHalf class=\"field-half away\" [class.hidden]=\"!showAway()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of awayZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-away\">\n @for (player of awayPlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n\n <!-- HOME half -->\n <div #homeHalf class=\"field-half home\" [class.hidden]=\"!showHome()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of homeZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-home\">\n @for (player of homePlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;height:100%}*{box-sizing:border-box}.soccer-board-wrapper{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .soccer-board-container{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .soccer-board-container{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .soccer-board-container{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .soccer-board-container{flex:1;min-height:0}.layout-no-sidebar .soccer-board-container{width:100%;height:100%}.players-sidebar{display:flex;flex-direction:column;background:transparent;padding:0;overflow-y:auto}.players-grid{display:grid;gap:.5rem;align-content:start}.soccer-board-container{width:100%;display:flex;justify-content:center;align-items:center;padding:var(--soccer-board-padding, 0);max-width:var(--soccer-board-max-width, none);width:var(--soccer-board-width, 100%);aspect-ratio:105/68}.field-container{display:flex;flex-direction:column;gap:0;width:100%;min-width:300px;height:auto}.field-container.landscape{flex-direction:row}.field-container.landscape .field-half{width:50%;aspect-ratio:52.5/68;background:repeating-linear-gradient(90deg,#2d5016 0px 10px,#3a6b1f 10px 20px)}.field-container.landscape .field-half.home{border-radius:0 10px 10px 0;box-shadow:10px 0 30px #0000004d}.field-container.landscape .field-half.away{border-radius:10px 0 0 10px;box-shadow:-5px 0 20px #0003}.field-container.landscape .field-half.home .center-border{inset:0 auto 0 0;width:3px;height:auto}.field-container.landscape .field-half.away .center-border{inset:0 0 0 auto;width:3px;height:auto}.field-container.landscape .field-half.home .half-circle{top:50%;left:0;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-left:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:0 999px 999px 0}.field-container.landscape .field-half.away .half-circle{inset:50% 0 auto auto;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-right:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:999px 0 0 999px}.field-container.landscape .field-half.home .penalty-area{inset:50% 0 auto auto;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .penalty-area{inset:50% auto auto 0;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-container.landscape .field-half.home .goal-area{inset:50% 0 auto auto;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .goal-area{inset:50% auto auto 0;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-half{position:relative;width:100%;aspect-ratio:68/52.5;background:repeating-linear-gradient(0deg,#2d5016 0px 10px,#3a6b1f 10px 20px);overflow:hidden}.field-half.home{border-radius:0 0 10px 10px;box-shadow:0 10px 30px #0000004d}.field-half.away{border-radius:10px 10px 0 0;box-shadow:0 -5px 20px #0003}.field-half.hidden{display:none}.field-lines{position:absolute;inset:0;pointer-events:none}.field-half.home .center-border{position:absolute;background:#fffc;z-index:10;top:0;left:0;right:0;height:3px}.field-half.away .center-border{position:absolute;background:#fffc;z-index:10;bottom:0;left:0;right:0;height:3px}.field-half.home .half-circle{position:absolute;z-index:5;top:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-top:none;border-radius:0 0 999px 999px;background:transparent}.field-half.away .half-circle{position:absolute;z-index:5;bottom:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-bottom:none;border-radius:999px 999px 0 0;background:transparent}.field-half.home .penalty-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .penalty-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-top:none}.field-half.home .goal-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .goal-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-top:none}.position-zones{position:absolute;inset:0;pointer-events:none;z-index:15}.position-zones.hidden{display:none}.position-zone{position:absolute;border:2px solid rgba(255,255,255,.8);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.8);opacity:.85;transition:all .3s ease}.position-zone:hover{opacity:1;z-index:100;transform:scale(1.05)}.players-container{position:absolute;inset:0;pointer-events:none;z-index:20}.players-container>*{pointer-events:auto}\n"] }]
1180
+ args: [{ selector: 'fuzo-soccer-board', standalone: true, imports: [SoccerBoardPlayerComponent], template: "<div class=\"soccer-board-wrapper\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n @if (showPlayersSidebar()) {\n <div class=\"players-sidebar\">\n <div\n class=\"players-grid\"\n [style.grid-template-columns]=\"playersGridColumns()\"\n >\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragStart)=\"onPlayerDragStart($event)\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Soccer Board Container -->\n <div #soccerBoardContainer class=\"soccer-board-container\">\n <div\n #fieldContainer\n class=\"field-container\"\n [class.landscape]=\"isLandscape()\"\n >\n <!-- AWAY half -->\n <div #awayHalf class=\"field-half away\" [class.hidden]=\"!showAway()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of awayZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-away\">\n @for (player of awayPlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [positionLeft]=\"getPlayerPosition(player).left\"\n [positionTop]=\"getPlayerPosition(player).top\"\n (dragStart)=\"onPlayerDragStart($event)\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n\n <!-- HOME half -->\n <div #homeHalf class=\"field-half home\" [class.hidden]=\"!showHome()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of homeZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-home\">\n @for (player of homePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [positionLeft]=\"getPlayerPosition(player).left\"\n [positionTop]=\"getPlayerPosition(player).top\"\n (dragStart)=\"onPlayerDragStart($event)\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;height:100%}*{box-sizing:border-box}.soccer-board-wrapper{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .soccer-board-container{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .soccer-board-container{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .soccer-board-container{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .soccer-board-container{flex:1;min-height:0}.layout-no-sidebar .soccer-board-container{width:100%;height:100%}.players-sidebar{display:flex;flex-direction:column;background:transparent;padding:0;overflow-y:auto}.players-grid{display:grid;gap:.5rem;align-content:start}.soccer-board-container{width:100%;display:flex;justify-content:center;align-items:center;padding:var(--soccer-board-padding, 0);max-width:var(--soccer-board-max-width, none);width:var(--soccer-board-width, 100%);aspect-ratio:105/68}.field-container{display:flex;flex-direction:column;gap:0;width:100%;min-width:300px;height:auto}.field-container.landscape{flex-direction:row}.field-container.landscape .field-half{width:50%;aspect-ratio:52.5/68;background:repeating-linear-gradient(90deg,#2d5016 0px 10px,#3a6b1f 10px 20px)}.field-container.landscape .field-half.home{border-radius:0 10px 10px 0;box-shadow:10px 0 30px #0000004d}.field-container.landscape .field-half.away{border-radius:10px 0 0 10px;box-shadow:-5px 0 20px #0003}.field-container.landscape .field-half.home .center-border{inset:0 auto 0 0;width:3px;height:auto}.field-container.landscape .field-half.away .center-border{inset:0 0 0 auto;width:3px;height:auto}.field-container.landscape .field-half.home .half-circle{top:50%;left:0;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-left:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:0 999px 999px 0}.field-container.landscape .field-half.away .half-circle{inset:50% 0 auto auto;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-right:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:999px 0 0 999px}.field-container.landscape .field-half.home .penalty-area{inset:50% 0 auto auto;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .penalty-area{inset:50% auto auto 0;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-container.landscape .field-half.home .goal-area{inset:50% 0 auto auto;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .goal-area{inset:50% auto auto 0;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-half{position:relative;width:100%;aspect-ratio:68/52.5;background:repeating-linear-gradient(0deg,#2d5016 0px 10px,#3a6b1f 10px 20px);overflow:hidden}.field-half.home{border-radius:0 0 10px 10px;box-shadow:0 10px 30px #0000004d}.field-half.away{border-radius:10px 10px 0 0;box-shadow:0 -5px 20px #0003}.field-half.hidden{display:none}.field-lines{position:absolute;inset:0;pointer-events:none}.field-half.home .center-border{position:absolute;background:#fffc;z-index:10;top:0;left:0;right:0;height:3px}.field-half.away .center-border{position:absolute;background:#fffc;z-index:10;bottom:0;left:0;right:0;height:3px}.field-half.home .half-circle{position:absolute;z-index:5;top:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-top:none;border-radius:0 0 999px 999px;background:transparent}.field-half.away .half-circle{position:absolute;z-index:5;bottom:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-bottom:none;border-radius:999px 999px 0 0;background:transparent}.field-half.home .penalty-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .penalty-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-top:none}.field-half.home .goal-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .goal-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-top:none}.position-zones{position:absolute;inset:0;pointer-events:none;z-index:15}.position-zones.hidden{display:none}.position-zone{position:absolute;border:2px solid rgba(255,255,255,.8);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.8);opacity:.85;transition:all .3s ease}.position-zone:hover{opacity:1;z-index:100;transform:scale(1.05)}.players-container{position:absolute;inset:0;pointer-events:none;z-index:20}.players-container>*{pointer-events:auto}\n"] }]
945
1181
  }], ctorParameters: () => [], propDecorators: { orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], teamSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "teamSide", required: false }] }], showPositions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPositions", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], players: [{ type: i0.Input, args: [{ isSignal: true, alias: "players", required: false }] }], showPlayersSidebar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPlayersSidebar", required: false }] }], playersPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "playersPosition", required: false }] }], playersColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "playersColumns", required: false }] }], maxPlayersPerSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxPlayersPerSide", required: false }] }], fieldContainer: [{ type: i0.ViewChild, args: ['fieldContainer', { isSignal: true }] }], homeHalf: [{ type: i0.ViewChild, args: ['homeHalf', { isSignal: true }] }], awayHalf: [{ type: i0.ViewChild, args: ['awayHalf', { isSignal: true }] }], soccerBoardContainer: [{ type: i0.ViewChild, args: ['soccerBoardContainer', { isSignal: true }] }], playerPositioned: [{ type: i0.Output, args: ["playerPositioned"] }], playerRemovedFromField: [{ type: i0.Output, args: ["playerRemovedFromField"] }], imageExported: [{ type: i0.Output, args: ["imageExported"] }], playerClicked: [{ type: i0.Output, args: ["playerClicked"] }], hostWidthStyle: [{
946
1182
  type: HostBinding,
947
1183
  args: ['style.width']
948
1184
  }] } });
949
1185
 
950
- var PlayersPosition;
951
- (function (PlayersPosition) {
952
- PlayersPosition["Left"] = "left";
953
- PlayersPosition["Right"] = "right";
954
- PlayersPosition["Top"] = "top";
955
- PlayersPosition["Bottom"] = "bottom";
956
- })(PlayersPosition || (PlayersPosition = {}));
957
- class SoccerBoardLayoutComponent {
958
- // Expose enums to template
959
- PlayersPosition = PlayersPosition;
960
- PlayerSize = SoccerBoardPlayerSize;
961
- // Inputs
962
- players = input.required(...(ngDevMode ? [{ debugName: "players" }] : []));
963
- orientation = input(SoccerBoardOrientation.Portrait, ...(ngDevMode ? [{ debugName: "orientation" }] : []));
964
- teamSide = input(null, ...(ngDevMode ? [{ debugName: "teamSide" }] : []));
965
- showPositions = input(false, ...(ngDevMode ? [{ debugName: "showPositions" }] : []));
966
- fit = input(null, ...(ngDevMode ? [{ debugName: "fit" }] : []));
967
- playersPosition = input(PlayersPosition.Left, ...(ngDevMode ? [{ debugName: "playersPosition" }] : []));
968
- playersColumns = input(2, ...(ngDevMode ? [{ debugName: "playersColumns" }] : [])); // Number of columns for players grid
969
- // ViewChild reference to soccer board
970
- soccerBoard = viewChild.required('soccerBoard');
971
- // Outputs
972
- playerPositioned = output();
973
- playerRemovedFromField = output();
974
- // Computed: players available (not on field)
975
- availablePlayers = computed(() => {
976
- return this.players().filter((player) => player.fieldX === undefined || player.fieldY === undefined);
977
- }, ...(ngDevMode ? [{ debugName: "availablePlayers" }] : []));
978
- // Computed: layout classes based on players position
979
- layoutClasses = computed(() => {
980
- const position = this.playersPosition();
981
- switch (position) {
982
- case PlayersPosition.Right:
983
- return 'layout-players-right';
984
- case PlayersPosition.Top:
985
- return 'layout-players-top';
986
- case PlayersPosition.Bottom:
987
- return 'layout-players-bottom';
988
- default:
989
- return 'layout-players-left';
990
- }
991
- }, ...(ngDevMode ? [{ debugName: "layoutClasses" }] : []));
992
- // Computed: players grid columns
993
- playersGridColumns = computed(() => {
994
- return `repeat(${this.playersColumns()}, minmax(0, 1fr))`;
995
- }, ...(ngDevMode ? [{ debugName: "playersGridColumns" }] : []));
996
- onPlayerDragEnd(event) {
997
- const board = this.soccerBoard();
998
- if (board) {
999
- board.handlePlayerDrop(event);
1000
- }
1001
- }
1002
- onPlayerPositioned(event) {
1003
- this.playerPositioned.emit(event);
1004
- }
1005
- onPlayerRemoveFromField(event) {
1006
- this.playerRemovedFromField.emit(event);
1007
- }
1008
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1009
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SoccerBoardLayoutComponent, isStandalone: true, selector: "lib-soccer-board-layout", inputs: { players: { classPropertyName: "players", publicName: "players", isSignal: true, isRequired: true, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, teamSide: { classPropertyName: "teamSide", publicName: "teamSide", isSignal: true, isRequired: false, transformFunction: null }, showPositions: { classPropertyName: "showPositions", publicName: "showPositions", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null }, playersPosition: { classPropertyName: "playersPosition", publicName: "playersPosition", isSignal: true, isRequired: false, transformFunction: null }, playersColumns: { classPropertyName: "playersColumns", publicName: "playersColumns", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playerPositioned: "playerPositioned", playerRemovedFromField: "playerRemovedFromField" }, viewQueries: [{ propertyName: "soccerBoard", first: true, predicate: ["soccerBoard"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"soccer-board-layout\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n <div class=\"players-sidebar\">\n <div class=\"players-header\">\n <h3 class=\"players-title\">Available Players</h3>\n <span class=\"players-count\">{{ availablePlayers().length }}</span>\n </div>\n <div class=\"players-grid\" [style.grid-template-columns]=\"playersGridColumns()\">\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n />\n }\n </div>\n </div>\n\n <!-- Soccer Board Area -->\n <div class=\"board-area\">\n <fuzo-soccer-board\n #soccerBoard\n [orientation]=\"orientation()\"\n [teamSide]=\"teamSide()\"\n [showPositions]=\"showPositions()\"\n [fit]=\"fit()\"\n [players]=\"players()\"\n (playerPositioned)=\"onPlayerPositioned($event)\"\n (playerRemovedFromField)=\"onPlayerRemoveFromField($event)\"\n />\n </div>\n</div>\n", styles: [".soccer-board-layout{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .board-area{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .board-area{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .board-area{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .board-area{flex:1;min-height:0}.players-sidebar{display:flex;flex-direction:column;background:#f5f5f5;border-radius:8px;padding:1rem;overflow-y:auto;box-shadow:0 2px 8px #0000001a}.players-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:2px solid #e5e5e5}.players-title{font-size:1.125rem;font-weight:600;color:#333;margin:0}.players-count{font-size:.875rem;color:#666;background:#e5e5e5;padding:.25rem .5rem;border-radius:12px;font-weight:500}.players-grid{display:grid;gap:1rem;align-content:start}\n"], dependencies: [{ kind: "component", type: SoccerBoardComponent, selector: "fuzo-soccer-board", inputs: ["orientation", "teamSide", "showPositions", "fit", "players", "showPlayersSidebar", "playersPosition", "playersColumns", "maxPlayersPerSide"], outputs: ["playerPositioned", "playerRemovedFromField", "imageExported", "playerClicked"] }, { kind: "component", type: SoccerBoardPlayerComponent, selector: "lib-soccer-board-player", inputs: ["player", "size"], outputs: ["dragStart", "dragging", "dragEnd", "removeFromField", "playerClicked"] }] });
1010
- }
1011
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardLayoutComponent, decorators: [{
1012
- type: Component,
1013
- args: [{ selector: 'lib-soccer-board-layout', standalone: true, imports: [SoccerBoardComponent, SoccerBoardPlayerComponent], template: "<div class=\"soccer-board-layout\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n <div class=\"players-sidebar\">\n <div class=\"players-header\">\n <h3 class=\"players-title\">Available Players</h3>\n <span class=\"players-count\">{{ availablePlayers().length }}</span>\n </div>\n <div class=\"players-grid\" [style.grid-template-columns]=\"playersGridColumns()\">\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n />\n }\n </div>\n </div>\n\n <!-- Soccer Board Area -->\n <div class=\"board-area\">\n <fuzo-soccer-board\n #soccerBoard\n [orientation]=\"orientation()\"\n [teamSide]=\"teamSide()\"\n [showPositions]=\"showPositions()\"\n [fit]=\"fit()\"\n [players]=\"players()\"\n (playerPositioned)=\"onPlayerPositioned($event)\"\n (playerRemovedFromField)=\"onPlayerRemoveFromField($event)\"\n />\n </div>\n</div>\n", styles: [".soccer-board-layout{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .board-area{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .board-area{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .board-area{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .board-area{flex:1;min-height:0}.players-sidebar{display:flex;flex-direction:column;background:#f5f5f5;border-radius:8px;padding:1rem;overflow-y:auto;box-shadow:0 2px 8px #0000001a}.players-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:2px solid #e5e5e5}.players-title{font-size:1.125rem;font-weight:600;color:#333;margin:0}.players-count{font-size:.875rem;color:#666;background:#e5e5e5;padding:.25rem .5rem;border-radius:12px;font-weight:500}.players-grid{display:grid;gap:1rem;align-content:start}\n"] }]
1014
- }], propDecorators: { players: [{ type: i0.Input, args: [{ isSignal: true, alias: "players", required: true }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], teamSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "teamSide", required: false }] }], showPositions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPositions", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], playersPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "playersPosition", required: false }] }], playersColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "playersColumns", required: false }] }], soccerBoard: [{ type: i0.ViewChild, args: ['soccerBoard', { isSignal: true }] }], playerPositioned: [{ type: i0.Output, args: ["playerPositioned"] }], playerRemovedFromField: [{ type: i0.Output, args: ["playerRemovedFromField"] }] } });
1015
-
1016
1186
  /**
1017
1187
  * Generated bundle index. Do not edit.
1018
1188
  */
1019
1189
 
1020
- export { PlayersPosition, SoccerBoardComponent, SoccerBoardFieldConstants, SoccerBoardFitMode, SoccerBoardLayoutComponent, SoccerBoardOrientation, SoccerBoardPlayerComponent, SoccerBoardPlayerSize, SoccerBoardTeamSide };
1190
+ export { SoccerBoardComponent, SoccerBoardFieldConstants, SoccerBoardFitMode, SoccerBoardOrientation, SoccerBoardPlayerComponent, SoccerBoardPlayerSize, SoccerBoardTeamSide };
1021
1191
  //# sourceMappingURL=fuzo-soccer-board.mjs.map