@fuzo/soccer-board 0.1.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.
@@ -0,0 +1,1021 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, input, output, signal, computed, effect, HostListener, Component, PLATFORM_ID, viewChild, HostBinding } from '@angular/core';
3
+ import { isPlatformBrowser } from '@angular/common';
4
+ import html2canvas from 'html2canvas-pro';
5
+
6
+ /**
7
+ * Soccer Board Field dimension constants
8
+ * Based on FIFA regulations: 105m x 68m
9
+ */
10
+ const SoccerBoardFieldConstants = {
11
+ /**
12
+ * Field aspect ratio (width / height)
13
+ * FIFA standard: 105m / 68m ≈ 1.544
14
+ */
15
+ RATIO: 105 / 68,
16
+ };
17
+
18
+ /**
19
+ * Soccer Board Fit Mode types for responsive field sizing
20
+ */
21
+ var SoccerBoardFitMode;
22
+ (function (SoccerBoardFitMode) {
23
+ SoccerBoardFitMode["Contain"] = "contain";
24
+ })(SoccerBoardFitMode || (SoccerBoardFitMode = {}));
25
+
26
+ /**
27
+ * Soccer Board Field Orientation types
28
+ */
29
+ var SoccerBoardOrientation;
30
+ (function (SoccerBoardOrientation) {
31
+ SoccerBoardOrientation["Portrait"] = "portrait";
32
+ SoccerBoardOrientation["Landscape"] = "landscape";
33
+ })(SoccerBoardOrientation || (SoccerBoardOrientation = {}));
34
+
35
+ /**
36
+ * Soccer Board Player Card Size types
37
+ */
38
+ var SoccerBoardPlayerSize;
39
+ (function (SoccerBoardPlayerSize) {
40
+ SoccerBoardPlayerSize["Small"] = "small";
41
+ SoccerBoardPlayerSize["Medium"] = "medium";
42
+ SoccerBoardPlayerSize["Large"] = "large";
43
+ })(SoccerBoardPlayerSize || (SoccerBoardPlayerSize = {}));
44
+
45
+ /**
46
+ * Soccer Board Team Side types
47
+ */
48
+ var SoccerBoardTeamSide;
49
+ (function (SoccerBoardTeamSide) {
50
+ SoccerBoardTeamSide["Home"] = "home";
51
+ SoccerBoardTeamSide["Away"] = "away";
52
+ })(SoccerBoardTeamSide || (SoccerBoardTeamSide = {}));
53
+
54
+ /**
55
+ * Get tactical position zones data
56
+ * All tactical positions with coordinates for each orientation
57
+ * Portrait: x=left-right, y=center(0)-goal(100)
58
+ * Landscape: x=center(0)-goal(100), y=top(0)-bottom(100)
59
+ */
60
+ function getPositionsData() {
61
+ // Colors by position type
62
+ const colors = {
63
+ goalkeeper: 'rgba(255, 200, 0, 0.5)', // Yellow
64
+ defense: 'rgba(70, 130, 255, 0.5)', // Blue
65
+ defensiveMid: 'rgba(100, 180, 255, 0.5)', // Light blue
66
+ midfield: 'rgba(50, 200, 100, 0.5)', // Green
67
+ attackingMid: 'rgba(150, 220, 100, 0.5)', // Light green
68
+ forward: 'rgba(255, 100, 100, 0.5)', // Red
69
+ striker: 'rgba(255, 50, 50, 0.5)', // Dark red
70
+ };
71
+ return [
72
+ // ============ DEFENSE ZONE ============
73
+ {
74
+ id: 'LB',
75
+ label: 'LB',
76
+ color: colors.defense,
77
+ portrait: { x: 0, y: 70, width: 20, height: 30 },
78
+ landscape: { x: 70, y: 0, width: 30, height: 20 },
79
+ },
80
+ {
81
+ id: 'CB',
82
+ label: 'CB',
83
+ color: colors.defense,
84
+ portrait: { x: 20, y: 70, width: 60, height: 19.5 },
85
+ landscape: { x: 70, y: 20, width: 19.5, height: 60 },
86
+ },
87
+ {
88
+ id: 'CB',
89
+ label: '',
90
+ color: colors.defense,
91
+ portrait: { x: 20, y: 89.5, width: 16.55, height: 10.5 },
92
+ landscape: { x: 89.5, y: 20, width: 10.5, height: 16.55 },
93
+ },
94
+ {
95
+ id: 'GK',
96
+ label: 'GK',
97
+ color: colors.goalkeeper,
98
+ portrait: { x: 36.55, y: 89.5, width: 26.9, height: 10.5 },
99
+ landscape: { x: 89.5, y: 36.55, width: 10.5, height: 26.9 },
100
+ },
101
+ {
102
+ id: 'CB',
103
+ label: '',
104
+ color: colors.defense,
105
+ portrait: { x: 63.45, y: 89.5, width: 16.55, height: 10.5 },
106
+ landscape: { x: 89.5, y: 63.45, width: 10.5, height: 16.55 },
107
+ },
108
+ {
109
+ id: 'RB',
110
+ label: 'RB',
111
+ color: colors.defense,
112
+ portrait: { x: 80, y: 70, width: 20, height: 30 },
113
+ landscape: { x: 70, y: 80, width: 30, height: 20 },
114
+ },
115
+ // ============ DEFENSIVE MIDFIELD ZONE ============
116
+ {
117
+ id: 'LWB',
118
+ label: 'LWB',
119
+ color: colors.defensiveMid,
120
+ portrait: { x: 0, y: 55, width: 25, height: 15 },
121
+ landscape: { x: 55, y: 0, width: 15, height: 25 },
122
+ },
123
+ {
124
+ id: 'CDM',
125
+ label: 'CDM',
126
+ color: colors.defensiveMid,
127
+ portrait: { x: 25, y: 55, width: 50, height: 15 },
128
+ landscape: { x: 55, y: 25, width: 15, height: 50 },
129
+ },
130
+ {
131
+ id: 'RWB',
132
+ label: 'RWB',
133
+ color: colors.defensiveMid,
134
+ portrait: { x: 75, y: 55, width: 25, height: 15 },
135
+ landscape: { x: 55, y: 75, width: 15, height: 25 },
136
+ },
137
+ // ============ CENTRAL MIDFIELD ZONE ============
138
+ {
139
+ id: 'LM',
140
+ label: 'LM',
141
+ color: colors.midfield,
142
+ portrait: { x: 0, y: 40, width: 25, height: 15 },
143
+ landscape: { x: 40, y: 0, width: 15, height: 25 },
144
+ },
145
+ {
146
+ id: 'CM',
147
+ label: 'CM',
148
+ color: colors.midfield,
149
+ portrait: { x: 25, y: 40, width: 50, height: 15 },
150
+ landscape: { x: 40, y: 25, width: 15, height: 50 },
151
+ },
152
+ {
153
+ id: 'RM',
154
+ label: 'RM',
155
+ color: colors.midfield,
156
+ portrait: { x: 75, y: 40, width: 25, height: 15 },
157
+ landscape: { x: 40, y: 75, width: 15, height: 25 },
158
+ },
159
+ // ============ ATTACKING ZONE ============
160
+ {
161
+ id: 'LW',
162
+ label: 'LW',
163
+ color: colors.forward,
164
+ portrait: { x: 0, y: 0, width: 25, height: 40 },
165
+ landscape: { x: 0, y: 0, width: 40, height: 25 },
166
+ },
167
+ {
168
+ id: 'CAM',
169
+ label: 'CAM',
170
+ color: colors.attackingMid,
171
+ portrait: { x: 25, y: 25, width: 50, height: 15 },
172
+ landscape: { x: 25, y: 25, width: 15, height: 50 },
173
+ },
174
+ {
175
+ id: 'RW',
176
+ label: 'RW',
177
+ color: colors.forward,
178
+ portrait: { x: 75, y: 0, width: 25, height: 40 },
179
+ landscape: { x: 0, y: 75, width: 40, height: 25 },
180
+ },
181
+ {
182
+ id: 'CF',
183
+ label: 'CF',
184
+ color: colors.forward,
185
+ portrait: { x: 25, y: 10, width: 50, height: 15 },
186
+ landscape: { x: 10, y: 25, width: 15, height: 50 },
187
+ },
188
+ {
189
+ id: 'ST',
190
+ label: 'ST',
191
+ color: colors.striker,
192
+ portrait: { x: 25, y: 0, width: 50, height: 10 },
193
+ landscape: { x: 0, y: 25, width: 10, height: 50 },
194
+ },
195
+ ];
196
+ }
197
+
198
+ class SoccerBoardPlayerComponent {
199
+ // Host element reference
200
+ hostElement = inject((ElementRef));
201
+ // Inputs using signals
202
+ player = input.required(...(ngDevMode ? [{ debugName: "player" }] : []));
203
+ size = input(SoccerBoardPlayerSize.Medium, ...(ngDevMode ? [{ debugName: "size" }] : []));
204
+ // Outputs
205
+ dragStart = output();
206
+ dragging = output();
207
+ dragEnd = output();
208
+ removeFromField = output();
209
+ playerClicked = output();
210
+ // Drag state signals
211
+ isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
212
+ dragOffsetX = signal(0, ...(ngDevMode ? [{ debugName: "dragOffsetX" }] : []));
213
+ 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" }] : []));
229
+ // Computed values
230
+ playerPhoto = computed(() => {
231
+ const photo = this.player()?.photo;
232
+ return photo || null;
233
+ }, ...(ngDevMode ? [{ debugName: "playerPhoto" }] : []));
234
+ playerName = computed(() => this.player()?.name || '', ...(ngDevMode ? [{ debugName: "playerName" }] : []));
235
+ playerNumber = computed(() => this.player()?.number, ...(ngDevMode ? [{ debugName: "playerNumber" }] : []));
236
+ // Determine if player is on the field (has fieldX and fieldY coordinates)
237
+ isOnField = computed(() => {
238
+ const player = this.player();
239
+ return player?.fieldX !== undefined && player?.fieldY !== undefined;
240
+ }, ...(ngDevMode ? [{ debugName: "isOnField" }] : []));
241
+ // Show fieldPosition if on field, otherwise show preferredPosition
242
+ playerPosition = computed(() => {
243
+ const player = this.player();
244
+ if (this.isOnField() && player?.fieldPosition) {
245
+ return player.fieldPosition;
246
+ }
247
+ return player?.preferredPosition;
248
+ }, ...(ngDevMode ? [{ debugName: "playerPosition" }] : []));
249
+ // Size classes
250
+ sizeClasses = computed(() => {
251
+ const size = this.size();
252
+ switch (size) {
253
+ case SoccerBoardPlayerSize.Small:
254
+ return 'w-12 h-12';
255
+ case SoccerBoardPlayerSize.Large:
256
+ return 'w-20 h-20';
257
+ default:
258
+ return 'w-16 h-16';
259
+ }
260
+ }, ...(ngDevMode ? [{ debugName: "sizeClasses" }] : []));
261
+ constructor() {
262
+ // Effect to apply drag styles
263
+ effect(() => {
264
+ const styles = this.dragStyles();
265
+ const element = this.hostElement.nativeElement;
266
+ element.style.opacity = styles.opacity;
267
+ element.style.cursor = styles.cursor;
268
+ element.style.zIndex = styles.zIndex;
269
+ });
270
+ }
271
+ onMouseDown(event) {
272
+ event.preventDefault();
273
+ this.startDrag(event.clientX, event.clientY);
274
+ }
275
+ onTouchStart(event) {
276
+ if (event.touches.length !== 1)
277
+ return;
278
+ event.preventDefault();
279
+ const touch = event.touches[0];
280
+ this.startDrag(touch.clientX, touch.clientY);
281
+ }
282
+ startDrag(clientX, clientY) {
283
+ console.log('[Player] Start drag');
284
+ const element = this.hostElement.nativeElement;
285
+ const rect = element.getBoundingClientRect();
286
+ this.dragOffsetX.set(clientX - rect.left);
287
+ this.dragOffsetY.set(clientY - rect.top);
288
+ this.isDragging.set(true);
289
+ const player = this.player();
290
+ console.log('[Player] Emitting dragStart', { playerId: player.id, playerName: player.name });
291
+ this.dragStart.emit({
292
+ playerId: player.id,
293
+ playerName: player.name,
294
+ });
295
+ }
296
+ onMouseMove(event) {
297
+ if (!this.isDragging())
298
+ return;
299
+ event.preventDefault();
300
+ this.handleDrag(event.clientX, event.clientY);
301
+ }
302
+ onTouchMove(event) {
303
+ if (!this.isDragging() || event.touches.length !== 1)
304
+ return;
305
+ event.preventDefault();
306
+ const touch = event.touches[0];
307
+ this.handleDrag(touch.clientX, touch.clientY);
308
+ }
309
+ handleDrag(clientX, clientY) {
310
+ const element = this.hostElement.nativeElement;
311
+ const offsetX = this.dragOffsetX();
312
+ const offsetY = this.dragOffsetY();
313
+ // Move visually during drag
314
+ element.style.position = 'fixed';
315
+ element.style.left = `${clientX - offsetX}px`;
316
+ element.style.top = `${clientY - offsetY}px`;
317
+ element.style.pointerEvents = 'none';
318
+ const player = this.player();
319
+ this.dragging.emit({
320
+ playerId: player.id,
321
+ clientX,
322
+ clientY,
323
+ offsetX,
324
+ offsetY,
325
+ });
326
+ }
327
+ onMouseUp(event) {
328
+ if (!this.isDragging())
329
+ return;
330
+ this.endDrag(event.clientX, event.clientY);
331
+ }
332
+ onTouchEnd(event) {
333
+ if (!this.isDragging())
334
+ return;
335
+ const touch = event.changedTouches[0];
336
+ this.endDrag(touch.clientX, touch.clientY);
337
+ }
338
+ endDrag(clientX, clientY) {
339
+ console.log('[Player] End drag', { clientX, clientY });
340
+ this.isDragging.set(false);
341
+ const element = this.hostElement.nativeElement;
342
+ element.style.pointerEvents = '';
343
+ element.style.position = '';
344
+ element.style.left = '';
345
+ element.style.top = '';
346
+ const player = this.player();
347
+ const dragEndEvent = {
348
+ playerId: player.id,
349
+ clientX,
350
+ clientY,
351
+ offsetX: this.dragOffsetX(),
352
+ offsetY: this.dragOffsetY(),
353
+ };
354
+ console.log('[Player] Emitting dragEnd', dragEndEvent);
355
+ this.dragEnd.emit(dragEndEvent);
356
+ }
357
+ onRemoveClick(event) {
358
+ event.stopPropagation();
359
+ event.preventDefault();
360
+ const player = this.player();
361
+ console.log('[Player] Remove from field clicked', player.id);
362
+ this.removeFromField.emit({ playerId: player.id });
363
+ }
364
+ onPlayerClick(event) {
365
+ // Don't emit click if we're dragging or if the click was on the remove button
366
+ if (this.isDragging()) {
367
+ return;
368
+ }
369
+ event.stopPropagation();
370
+ const player = this.player();
371
+ console.log('[Player] Player clicked', player.id);
372
+ this.playerClicked.emit({ playerId: player.id, player });
373
+ }
374
+ 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"] });
376
+ }
377
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardPlayerComponent, decorators: [{
378
+ 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: [{
381
+ type: HostListener,
382
+ args: ['mousedown', ['$event']]
383
+ }], onTouchStart: [{
384
+ type: HostListener,
385
+ args: ['touchstart', ['$event']]
386
+ }], onMouseMove: [{
387
+ type: HostListener,
388
+ args: ['document:mousemove', ['$event']]
389
+ }], onTouchMove: [{
390
+ type: HostListener,
391
+ args: ['document:touchmove', ['$event']]
392
+ }], onMouseUp: [{
393
+ type: HostListener,
394
+ args: ['document:mouseup', ['$event']]
395
+ }], onTouchEnd: [{
396
+ type: HostListener,
397
+ args: ['document:touchend', ['$event']]
398
+ }] } });
399
+
400
+ /**
401
+ * Soccer Board Component
402
+ * Renders a FIFA-compliant soccer field with two halves (HOME and AWAY)
403
+ * Supports portrait and landscape orientations
404
+ */
405
+ class SoccerBoardComponent {
406
+ // Host element reference
407
+ hostElement = inject((ElementRef));
408
+ platformId = inject(PLATFORM_ID);
409
+ // Expose SoccerBoardPlayerSize to template
410
+ PlayerSize = SoccerBoardPlayerSize;
411
+ // Inputs using signals
412
+ orientation = input(SoccerBoardOrientation.Landscape, ...(ngDevMode ? [{ debugName: "orientation" }] : []));
413
+ teamSide = input(null, ...(ngDevMode ? [{ debugName: "teamSide" }] : []));
414
+ showPositions = input(false, ...(ngDevMode ? [{ debugName: "showPositions" }] : []));
415
+ fit = input(null, ...(ngDevMode ? [{ debugName: "fit" }] : []));
416
+ players = input([], ...(ngDevMode ? [{ debugName: "players" }] : []));
417
+ // Layout configuration
418
+ showPlayersSidebar = input(true, ...(ngDevMode ? [{ debugName: "showPlayersSidebar" }] : []));
419
+ playersPosition = input('left', ...(ngDevMode ? [{ debugName: "playersPosition" }] : []));
420
+ playersColumns = input(2, ...(ngDevMode ? [{ debugName: "playersColumns" }] : []));
421
+ // Maximum players per side (default: 7)
422
+ maxPlayersPerSide = input(7, ...(ngDevMode ? [{ debugName: "maxPlayersPerSide" }] : []));
423
+ // ViewChild references
424
+ fieldContainer = viewChild('fieldContainer', ...(ngDevMode ? [{ debugName: "fieldContainer" }] : []));
425
+ homeHalf = viewChild('homeHalf', ...(ngDevMode ? [{ debugName: "homeHalf" }] : []));
426
+ awayHalf = viewChild('awayHalf', ...(ngDevMode ? [{ debugName: "awayHalf" }] : []));
427
+ soccerBoardContainer = viewChild('soccerBoardContainer', ...(ngDevMode ? [{ debugName: "soccerBoardContainer" }] : []));
428
+ // Outputs
429
+ playerPositioned = output();
430
+ playerRemovedFromField = output();
431
+ imageExported = output();
432
+ playerClicked = output();
433
+ // Method to handle player drag end (called from template)
434
+ onPlayerDragEnd(event) {
435
+ this.handlePlayerDrop(event);
436
+ }
437
+ // Method to handle player click (called from template)
438
+ onPlayerClick(event) {
439
+ console.log('[SoccerBoard] Player clicked', event.playerId);
440
+ this.playerClicked.emit(event);
441
+ }
442
+ // Method to handle player remove from field (called from template)
443
+ onPlayerRemoveFromField(event) {
444
+ // Emit event to parent to handle removal
445
+ console.log('[SoccerBoard] Player remove requested', event.playerId);
446
+ this.playerRemovedFromField.emit({ playerId: event.playerId });
447
+ }
448
+ // Reactive width signal for host element
449
+ hostWidth = signal('', ...(ngDevMode ? [{ debugName: "hostWidth" }] : []));
450
+ // Host binding for width (reactive)
451
+ get hostWidthStyle() {
452
+ return this.hostWidth();
453
+ }
454
+ // Computed values
455
+ isLandscape = computed(() => this.orientation() === SoccerBoardOrientation.Landscape, ...(ngDevMode ? [{ debugName: "isLandscape" }] : []));
456
+ showHome = computed(() => {
457
+ const side = this.teamSide();
458
+ return side === null || side === SoccerBoardTeamSide.Home;
459
+ }, ...(ngDevMode ? [{ debugName: "showHome" }] : []));
460
+ showAway = computed(() => {
461
+ const side = this.teamSide();
462
+ return side === null || side === SoccerBoardTeamSide.Away;
463
+ }, ...(ngDevMode ? [{ debugName: "showAway" }] : []));
464
+ // Computed position zones
465
+ homeZones = computed(() => {
466
+ if (!this.showPositions()) {
467
+ return [];
468
+ }
469
+ const positionsData = getPositionsData();
470
+ const orientation = this.orientation();
471
+ return positionsData.map((pos) => ({
472
+ ...pos,
473
+ coords: pos[orientation],
474
+ }));
475
+ }, ...(ngDevMode ? [{ debugName: "homeZones" }] : []));
476
+ awayZones = computed(() => {
477
+ if (!this.showPositions()) {
478
+ return [];
479
+ }
480
+ const positionsData = getPositionsData();
481
+ const orientation = this.orientation();
482
+ return positionsData.map((pos) => {
483
+ const coords = pos[orientation];
484
+ let awayCoords;
485
+ if (orientation === SoccerBoardOrientation.Portrait) {
486
+ // Portrait: invert Y
487
+ awayCoords = {
488
+ ...coords,
489
+ y: 100 - coords.y - coords.height,
490
+ };
491
+ }
492
+ else {
493
+ // Landscape: invert X
494
+ awayCoords = {
495
+ ...coords,
496
+ x: 100 - coords.x - coords.width,
497
+ };
498
+ }
499
+ return {
500
+ ...pos,
501
+ coords: awayCoords,
502
+ };
503
+ });
504
+ }, ...(ngDevMode ? [{ debugName: "awayZones" }] : []));
505
+ // Computed players by side
506
+ homePlayers = computed(() => {
507
+ return this.players().filter((player) => player.side === SoccerBoardTeamSide.Home && player.fieldX !== undefined && player.fieldY !== undefined);
508
+ }, ...(ngDevMode ? [{ debugName: "homePlayers" }] : []));
509
+ awayPlayers = computed(() => {
510
+ return this.players().filter((player) => player.side === SoccerBoardTeamSide.Away && player.fieldX !== undefined && player.fieldY !== undefined);
511
+ }, ...(ngDevMode ? [{ debugName: "awayPlayers" }] : []));
512
+ // Computed: players available (not on field)
513
+ availablePlayers = computed(() => {
514
+ return this.players().filter((player) => player.fieldX === undefined || player.fieldY === undefined);
515
+ }, ...(ngDevMode ? [{ debugName: "availablePlayers" }] : []));
516
+ // Computed: layout classes based on players position
517
+ layoutClasses = computed(() => {
518
+ if (!this.showPlayersSidebar()) {
519
+ return 'layout-no-sidebar';
520
+ }
521
+ const position = this.playersPosition();
522
+ switch (position) {
523
+ case 'right':
524
+ return 'layout-players-right';
525
+ case 'top':
526
+ return 'layout-players-top';
527
+ case 'bottom':
528
+ return 'layout-players-bottom';
529
+ default:
530
+ return 'layout-players-left';
531
+ }
532
+ }, ...(ngDevMode ? [{ debugName: "layoutClasses" }] : []));
533
+ // Computed: players grid columns
534
+ playersGridColumns = computed(() => {
535
+ return `repeat(${this.playersColumns()}, minmax(0, 1fr))`;
536
+ }, ...(ngDevMode ? [{ debugName: "playersGridColumns" }] : []));
537
+ // Helper method to get visual position for a player (public for template)
538
+ getPlayerPosition(player) {
539
+ if (player.fieldX === undefined || player.fieldY === undefined || !player.side) {
540
+ return { left: 0, top: 0 };
541
+ }
542
+ return this.fieldToHalfCoordinates(player.fieldX, player.fieldY, player.side);
543
+ }
544
+ resizeObserver = null;
545
+ constructor() {
546
+ // Only run effects in browser (SSR protection)
547
+ if (isPlatformBrowser(this.platformId)) {
548
+ // Effect to update field size when fit or orientation changes
549
+ effect(() => {
550
+ this.fit();
551
+ this.orientation();
552
+ // Use setTimeout to ensure view is initialized
553
+ setTimeout(() => {
554
+ this.setupResizeObserver();
555
+ this.updateFieldSize();
556
+ }, 0);
557
+ });
558
+ // Effect to watch for --soccer-board-width CSS custom property changes
559
+ effect(() => {
560
+ // Trigger update when orientation changes (affects width calculation)
561
+ this.orientation();
562
+ if (this.fit() !== SoccerBoardFitMode.Contain) {
563
+ this.updateFieldSizeFromCSS();
564
+ }
565
+ });
566
+ }
567
+ }
568
+ /**
569
+ * Handles player drag end event
570
+ * Called from parent component when a player is dropped
571
+ * @public
572
+ */
573
+ handlePlayerDrop(event) {
574
+ console.log('[SoccerBoard] handlePlayerDrop called', event);
575
+ const position = this.calculateFieldPosition(event.clientX, event.clientY);
576
+ 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 {
609
+ console.warn('[SoccerBoard] Position is null - player not dropped on field');
610
+ }
611
+ }
612
+ /**
613
+ * Calculates field position from client coordinates
614
+ */
615
+ calculateFieldPosition(clientX, clientY) {
616
+ const homeHalfEl = this.homeHalf()?.nativeElement;
617
+ const awayHalfEl = this.awayHalf()?.nativeElement;
618
+ if (!homeHalfEl || !awayHalfEl) {
619
+ return null;
620
+ }
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;
626
+ // Check HOME half
627
+ const homeRect = homeHalfEl.getBoundingClientRect();
628
+ const isInHome = centerX >= homeRect.left &&
629
+ centerX <= homeRect.right &&
630
+ centerY >= homeRect.top &&
631
+ centerY <= homeRect.bottom;
632
+ // Check AWAY half
633
+ const awayRect = awayHalfEl.getBoundingClientRect();
634
+ const isInAway = centerX >= awayRect.left &&
635
+ centerX <= awayRect.right &&
636
+ centerY >= awayRect.top &&
637
+ centerY <= awayRect.bottom;
638
+ if (!isInHome && !isInAway) {
639
+ return null; // Not over the field
640
+ }
641
+ const side = isInHome ? SoccerBoardTeamSide.Home : SoccerBoardTeamSide.Away;
642
+ const halfRect = isInHome ? homeRect : awayRect;
643
+ // Calculate position within the half (0-100%)
644
+ const relativeX = ((centerX - halfRect.left) / halfRect.width) * 100;
645
+ const relativeY = ((centerY - halfRect.top) / halfRect.height) * 100;
646
+ // Clamp to 0-100%
647
+ const clampedX = Math.max(0, Math.min(100, relativeX));
648
+ const clampedY = Math.max(0, Math.min(100, relativeY));
649
+ // Convert to field coordinates (normalized)
650
+ const orientation = this.orientation();
651
+ let fieldX;
652
+ let fieldY;
653
+ if (orientation === SoccerBoardOrientation.Portrait) {
654
+ // Portrait: X is width, Y is length
655
+ fieldX = clampedX;
656
+ if (side === SoccerBoardTeamSide.Home) {
657
+ // HOME is bottom half (50-100% of field length)
658
+ fieldY = 50 + clampedY / 2;
659
+ }
660
+ else {
661
+ // AWAY is top half (0-50% of field length)
662
+ fieldY = clampedY / 2;
663
+ }
664
+ }
665
+ else {
666
+ // Landscape: X is length, Y is width
667
+ fieldY = clampedY; // Y is the width (top-bottom in visual space)
668
+ if (side === SoccerBoardTeamSide.Home) {
669
+ // HOME is right half (50-100% of field length)
670
+ fieldX = 50 + clampedX / 2;
671
+ }
672
+ else {
673
+ // AWAY is left half (0-50% of field length)
674
+ fieldX = clampedX / 2;
675
+ }
676
+ }
677
+ return { fieldX, fieldY, side };
678
+ }
679
+ /**
680
+ * Converts field coordinates to half coordinates
681
+ */
682
+ fieldToHalfCoordinates(fieldX, fieldY, side) {
683
+ const orientation = this.orientation();
684
+ let left;
685
+ let top;
686
+ if (orientation === SoccerBoardOrientation.Portrait) {
687
+ // Portrait mode
688
+ left = fieldX;
689
+ if (side === SoccerBoardTeamSide.Home) {
690
+ // HOME: fieldY 50-100% maps to top 0-100%
691
+ top = (fieldY - 50) * 2;
692
+ }
693
+ else {
694
+ // AWAY: fieldY 0-50% maps to top 0-100%
695
+ top = fieldY * 2;
696
+ }
697
+ }
698
+ else {
699
+ // Landscape mode
700
+ top = fieldY;
701
+ if (side === SoccerBoardTeamSide.Home) {
702
+ // HOME: fieldX 50-100% maps to left 0-100%
703
+ left = (fieldX - 50) * 2;
704
+ }
705
+ else {
706
+ // AWAY: fieldX 0-50% maps to left 0-100%
707
+ left = fieldX * 2;
708
+ }
709
+ }
710
+ return { left, top };
711
+ }
712
+ /**
713
+ * Detects tactical position based on field coordinates
714
+ */
715
+ detectTacticalPosition(fieldX, fieldY, side) {
716
+ // Convert field coordinates to half coordinates
717
+ const { left: halfX, top: halfY } = this.fieldToHalfCoordinates(fieldX, fieldY, side);
718
+ // Get position data (same as show-positions)
719
+ const positionsData = getPositionsData();
720
+ const orientation = this.orientation();
721
+ // Find which position zone contains this point
722
+ let detectedPosition = 'CM'; // Default fallback
723
+ let smallestArea = Infinity;
724
+ for (const pos of positionsData) {
725
+ let coords = pos[orientation];
726
+ // For AWAY, we need to invert coordinates (same logic as renderPositionZones)
727
+ if (side === SoccerBoardTeamSide.Away) {
728
+ if (orientation === SoccerBoardOrientation.Portrait) {
729
+ // Portrait: invert Y
730
+ coords = {
731
+ ...coords,
732
+ y: 100 - coords.y - coords.height,
733
+ };
734
+ }
735
+ else {
736
+ // Landscape: invert X
737
+ coords = {
738
+ ...coords,
739
+ x: 100 - coords.x - coords.width,
740
+ };
741
+ }
742
+ }
743
+ // Check if point is inside this zone
744
+ const isInside = halfX >= coords.x &&
745
+ halfX <= coords.x + coords.width &&
746
+ halfY >= coords.y &&
747
+ halfY <= coords.y + coords.height;
748
+ if (isInside) {
749
+ // If multiple zones overlap, prefer the smallest one (most specific)
750
+ const area = coords.width * coords.height;
751
+ if (area < smallestArea) {
752
+ smallestArea = area;
753
+ detectedPosition = pos.id;
754
+ }
755
+ }
756
+ }
757
+ return detectedPosition;
758
+ }
759
+ ngAfterViewInit() {
760
+ if (!isPlatformBrowser(this.platformId)) {
761
+ return;
762
+ }
763
+ // Initial setup
764
+ setTimeout(() => {
765
+ this.updateFieldSize();
766
+ this.setupResizeObserver();
767
+ }, 0);
768
+ }
769
+ ngOnDestroy() {
770
+ this.cleanupResizeObserver();
771
+ }
772
+ setupResizeObserver() {
773
+ if (!isPlatformBrowser(this.platformId)) {
774
+ return;
775
+ }
776
+ this.cleanupResizeObserver();
777
+ if (this.fit() !== SoccerBoardFitMode.Contain) {
778
+ return;
779
+ }
780
+ // Observe the host element's parent (like in the original)
781
+ const parentElement = this.hostElement.nativeElement.parentElement;
782
+ if (!parentElement) {
783
+ return;
784
+ }
785
+ this.resizeObserver = new ResizeObserver(() => {
786
+ this.updateFieldSize();
787
+ });
788
+ this.resizeObserver.observe(parentElement);
789
+ }
790
+ cleanupResizeObserver() {
791
+ if (this.resizeObserver) {
792
+ this.resizeObserver.disconnect();
793
+ this.resizeObserver = null;
794
+ }
795
+ }
796
+ updateFieldSize() {
797
+ if (!isPlatformBrowser(this.platformId)) {
798
+ return;
799
+ }
800
+ const hostElement = this.hostElement.nativeElement;
801
+ // If fit="contain", calculate size based on parent container (like original)
802
+ if (this.fit() === SoccerBoardFitMode.Contain && hostElement.parentElement) {
803
+ const parent = hostElement.parentElement;
804
+ const parentStyle = getComputedStyle(parent);
805
+ const parentWidth = parent.clientWidth -
806
+ parseFloat(parentStyle.paddingLeft) -
807
+ parseFloat(parentStyle.paddingRight);
808
+ const parentHeight = parent.clientHeight -
809
+ parseFloat(parentStyle.paddingTop) -
810
+ parseFloat(parentStyle.paddingBottom);
811
+ if (parentWidth > 0 && parentHeight > 0) {
812
+ const fieldAspectRatio = this.orientation() === SoccerBoardOrientation.Landscape
813
+ ? SoccerBoardFieldConstants.RATIO
814
+ : 1 / SoccerBoardFieldConstants.RATIO;
815
+ const containerAspectRatio = parentWidth / parentHeight;
816
+ let width;
817
+ if (containerAspectRatio > fieldAspectRatio) {
818
+ // Container is wider - height is the constraint
819
+ const height = parentHeight;
820
+ width = height * fieldAspectRatio;
821
+ }
822
+ else {
823
+ // Container is taller - width is the constraint
824
+ width = parentWidth;
825
+ }
826
+ // Update width signal reactively
827
+ this.hostWidth.set(`${width}px`);
828
+ return;
829
+ }
830
+ }
831
+ // Otherwise, use --soccer-board-width CSS custom property (like original)
832
+ this.updateFieldSizeFromCSS();
833
+ }
834
+ updateFieldSizeFromCSS() {
835
+ if (!isPlatformBrowser(this.platformId)) {
836
+ return;
837
+ }
838
+ const hostElement = this.hostElement.nativeElement;
839
+ const computedStyle = getComputedStyle(hostElement);
840
+ const fieldWidth = computedStyle.getPropertyValue('--soccer-board-width').trim();
841
+ if (!fieldWidth || fieldWidth === '100%') {
842
+ // Default behavior: 100% width, let CSS handle it
843
+ this.hostWidth.set(fieldWidth || '100%');
844
+ return;
845
+ }
846
+ // Parse the width value
847
+ const match = fieldWidth.match(/^([\d.]+)(px|rem|em|vw|vh|%)?$/);
848
+ if (!match) {
849
+ this.hostWidth.set(fieldWidth);
850
+ return;
851
+ }
852
+ const value = parseFloat(match[1]);
853
+ const unit = match[2] || 'px';
854
+ if (this.orientation() === SoccerBoardOrientation.Landscape) {
855
+ // In landscape, the CSS width represents the field length (105m)
856
+ // So we multiply the physical width (68m) by the ratio to get the CSS width
857
+ const landscapeWidth = value * SoccerBoardFieldConstants.RATIO;
858
+ this.hostWidth.set(`${landscapeWidth}${unit}`);
859
+ }
860
+ else {
861
+ // In portrait, the CSS width represents the field width (68m)
862
+ this.hostWidth.set(`${value}${unit}`);
863
+ }
864
+ }
865
+ /**
866
+ * Exports the soccer board field (without sidebar) to PNG image
867
+ * @public
868
+ */
869
+ async exportToPNG() {
870
+ if (!isPlatformBrowser(this.platformId)) {
871
+ console.warn('[SoccerBoard] Export to PNG is only available in browser');
872
+ return;
873
+ }
874
+ const container = this.fieldContainer()?.nativeElement;
875
+ if (!container) {
876
+ console.error('[SoccerBoard] Field container not found for export');
877
+ return;
878
+ }
879
+ try {
880
+ console.log('[SoccerBoard] Starting PNG export...');
881
+ // Wait for all images to load before exporting
882
+ const images = container.querySelectorAll('img');
883
+ const imagePromises = Array.from(images).map((img) => {
884
+ if (img.complete) {
885
+ return Promise.resolve();
886
+ }
887
+ return new Promise((resolve) => {
888
+ img.onload = () => resolve();
889
+ img.onerror = () => resolve(); // Continue even if image fails to load
890
+ // Timeout after 5 seconds
891
+ setTimeout(() => resolve(), 5000);
892
+ });
893
+ });
894
+ await Promise.all(imagePromises);
895
+ console.log('[SoccerBoard] All images loaded, capturing...');
896
+ // Use html2canvas-pro to capture the field (supports oklab/oklch colors)
897
+ const canvas = await html2canvas(container, {
898
+ backgroundColor: null, // Transparent background
899
+ scale: 2, // Higher quality
900
+ logging: false,
901
+ useCORS: true, // Allow cross-origin images
902
+ allowTaint: false,
903
+ width: container.offsetWidth,
904
+ height: container.offsetHeight,
905
+ ignoreElements: (element) => {
906
+ // Ignore remove badges
907
+ return element.classList?.contains('remove-badge') || false;
908
+ },
909
+ });
910
+ // Convert canvas to blob and emit event
911
+ canvas.toBlob((blob) => {
912
+ if (!blob) {
913
+ console.error('[SoccerBoard] Failed to create blob');
914
+ return;
915
+ }
916
+ // Get data URL for the image
917
+ const dataUrl = canvas.toDataURL('image/png');
918
+ // Emit event with blob and data URL
919
+ this.imageExported.emit({
920
+ blob,
921
+ dataUrl,
922
+ });
923
+ // Also trigger automatic download (optional - app can handle it via event)
924
+ const url = URL.createObjectURL(blob);
925
+ const link = document.createElement('a');
926
+ link.href = url;
927
+ link.download = `soccer-board-${Date.now()}.png`;
928
+ document.body.appendChild(link);
929
+ link.click();
930
+ document.body.removeChild(link);
931
+ URL.revokeObjectURL(url);
932
+ console.log('[SoccerBoard] PNG exported successfully');
933
+ }, 'image/png');
934
+ }
935
+ catch (error) {
936
+ console.error('[SoccerBoard] Error exporting to PNG:', error);
937
+ }
938
+ }
939
+ 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"] }] });
941
+ }
942
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardComponent, decorators: [{
943
+ 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"] }]
945
+ }], 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
+ type: HostBinding,
947
+ args: ['style.width']
948
+ }] } });
949
+
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
+ /**
1017
+ * Generated bundle index. Do not edit.
1018
+ */
1019
+
1020
+ export { PlayersPosition, SoccerBoardComponent, SoccerBoardFieldConstants, SoccerBoardFitMode, SoccerBoardLayoutComponent, SoccerBoardOrientation, SoccerBoardPlayerComponent, SoccerBoardPlayerSize, SoccerBoardTeamSide };
1021
+ //# sourceMappingURL=fuzo-soccer-board.mjs.map