@usssa/component-library 1.0.0-beta.9 → 1.0.0-rc.1

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.
@@ -5,7 +5,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
5
5
  import { UBtnIcon, UBtnStd, UChip, UMenuItem, USheet } from '../../components'
6
6
  import { useScreenType } from '../../composables/useScreenType'
7
7
 
8
- const emit = defineEmits(['open-expand'])
8
+ const emit = defineEmits(['open-expand', 'schedule-click'])
9
9
 
10
10
  defineOptions({
11
11
  name: 'UBracket',
@@ -52,10 +52,15 @@ const props = defineProps({
52
52
  teamSheetApplyLabel: { type: String, default: 'Select Team' },
53
53
  teamSheetCancelLabel: { type: String, default: 'Cancel' },
54
54
  teamSheetHeading: { type: String, default: 'Select Team' },
55
+ venueSheetHeading: { type: String, default: 'Venue Detail' },
55
56
  vGap: {
56
57
  type: Number,
57
58
  default: 50,
58
59
  },
60
+ viewMatchupDetailText: {
61
+ type: String,
62
+ default: 'View Matchup Details',
63
+ },
59
64
  zoomInTooltip: {
60
65
  type: String,
61
66
  default: 'Zoom In',
@@ -73,6 +78,7 @@ const bracketSides = [
73
78
  ]
74
79
  const nodeHeight = 40
75
80
  const nodeWidth = 240
81
+
76
82
  const self = 'bottom middle'
77
83
  const svgWidth = 1400
78
84
 
@@ -80,6 +86,12 @@ const activeRound = ref({ roundNo: 1, columnNo: 3 })
80
86
  const bracketSheet = ref([])
81
87
  const currentTransform = ref(d3.zoomIdentity)
82
88
  const gRef = ref(null)
89
+ const layoutCache = ref({
90
+ contentWidth: 0,
91
+ contentHeight: 0,
92
+ maxRow: 0,
93
+ numColumns: 0,
94
+ })
83
95
  const nodeGridPositions = ref({})
84
96
  const openMenu = ref(false)
85
97
  const openRoundMenu = ref(false)
@@ -92,6 +104,7 @@ const tempSelectedTeamId = ref('')
92
104
  const teamSheet = ref([])
93
105
  const tooltipState = ref({ visible: false, x: 0, y: 0, text: '' })
94
106
  const zoomRef = ref(null)
107
+ const locationSheet = ref([])
95
108
 
96
109
  const selectedTeamName = computed({
97
110
  get() {
@@ -109,67 +122,62 @@ const isTeamSelected = computed(() => selectedTeamName.value.length > 0)
109
122
 
110
123
  // Flatten for rendering logic
111
124
  const games = computed(() =>
112
- props.bracketData.round.flatMap((r, i) =>
125
+ normalizedBracketData.value.round.flatMap((r, i) =>
113
126
  r.game.map((m) => ({ ...m, round: r.roundNo }))
114
127
  )
115
128
  )
116
129
 
130
+ // Use normalized bracket data for all rendering and layout logic
131
+ const normalizedBracketData = computed(() =>
132
+ normalizeBracketData(props.bracketData)
133
+ )
134
+
117
135
  const roundLabelStyles = computed(() => {
118
136
  const t = currentTransform.value || d3.zoomIdentity
119
-
120
- // 1. Group rounds by column number to handle multiple rounds in the same visual column
121
- const roundsByColumn = props.bracketData.round.reduce((acc, round) => {
122
- const col = round.columnNo
123
- if (!acc[col]) {
124
- acc[col] = []
125
- }
126
- acc[col].push(round)
127
- return acc
128
- }, {})
129
-
130
- // 2. Get sorted column numbers
131
- const sortedColumns = Object.keys(roundsByColumn).sort((a, b) => a - b)
132
-
137
+ // Group rounds by normalized column number
138
+ const roundsByColumn = normalizedBracketData.value.round.reduce(
139
+ (acc, round) => {
140
+ const col = Number(round.columnNo)
141
+ if (!acc[col]) {
142
+ acc[col] = []
143
+ }
144
+ acc[col].push(round)
145
+ return acc
146
+ },
147
+ {}
148
+ )
149
+ // Get sorted normalized column numbers
150
+ const sortedColumns = Object.keys(roundsByColumn)
151
+ .map(Number)
152
+ .sort((a, b) => a - b)
133
153
  const styles = []
134
-
135
- // 3. Generate styles for each round
136
- props.bracketData.round.forEach((round) => {
137
- const col = round.columnNo
138
- const columnIndex = sortedColumns.indexOf(col.toString())
154
+ // Generate styles for each round using normalized column
155
+ normalizedBracketData.value.round.forEach((round, idx) => {
156
+ const col = Number(round.columnNo)
157
+ const columnIndex = sortedColumns.indexOf(col)
139
158
  const isLastColumn = columnIndex === sortedColumns.length - 1
140
-
141
159
  // The base X position is the same for all rounds in the same column
142
160
  const baseX = (col - 1) * props.hGap
143
-
144
- // The width of the label container should be the horizontal gap
145
161
  const baseWidth = props.hGap
146
-
147
162
  // Apply zoom and pan transformations
148
163
  const left = t.applyX(baseX)
149
164
  const width = baseWidth * t.k
150
-
151
165
  const style = {
152
166
  left: left + 'px',
153
167
  width: width + 'px',
154
168
  }
155
-
156
- // Add a border to all but the last column
157
169
  if (!isLastColumn) {
158
170
  style.borderRight = '1px solid #B9E0FF'
159
171
  }
160
-
161
- // Find the original index to push the style in the correct order for the template v-for
162
- const originalIndex = props.bracketData.round.findIndex((r) => r === round)
163
- styles[originalIndex] = style
172
+ styles[idx] = style
164
173
  })
165
-
166
174
  return styles
167
175
  })
168
176
 
169
- // Compute teams for toolbar selection from bracket data (selectionType: 'Seed')
177
+ // Compute teams for toolbar selection from normalized bracket data (selectionType: 'Seed')
170
178
  const toolbarTeams = computed(() => {
171
179
  const seedTeams = []
172
- props.bracketData.round.forEach((round) => {
180
+ normalizedBracketData.value.round.forEach((round) => {
173
181
  if (Array.isArray(round.game)) {
174
182
  round.game.forEach((game) => {
175
183
  if (Array.isArray(game.team)) {
@@ -200,7 +208,7 @@ const applyBracketSheetSelection = () => {
200
208
 
201
209
  const applyRoundSheetSelection = () => {
202
210
  const selectedRound = props.bracketData.round.find(
203
- (r) => `${r.roundNo}-${r.columnNo}` === tempSelectedRoundId.value
211
+ (r) => `${r.roundNo}-${r.roundName}` === tempSelectedRoundId.value
204
212
  )
205
213
  if (selectedRound) scrollToRound(selectedRound)
206
214
  roundSheet.value = []
@@ -209,6 +217,7 @@ const applyRoundSheetSelection = () => {
209
217
  const applyTeamSheetSelection = () => {
210
218
  selectedTeamId.value = tempSelectedTeamId.value
211
219
  teamSheet.value = []
220
+ resetZoom()
212
221
  renderBracket()
213
222
  }
214
223
 
@@ -225,13 +234,12 @@ const cancelTeamSheetSelection = () => {
225
234
  tempSelectedTeamId.value = selectedTeamId.value
226
235
  }
227
236
 
228
- // Compute all node positions using a row/column approach
237
+ // Compute all node positions and cache layout values
229
238
  const computeBracketLayout = () => {
230
239
  const newPositions = {}
231
240
 
232
241
  // Scan through all games to find every node with an explicit position
233
242
  games.value.forEach((game) => {
234
- // 1. Position the source teams for the game
235
243
  game.team.forEach((team) => {
236
244
  if (typeof team === 'object' && team.teamName && team.position) {
237
245
  const compositeId = `${team.teamName}_${team.position.columnNo}_${team.position.rowNo}`
@@ -241,8 +249,6 @@ const computeBracketLayout = () => {
241
249
  }
242
250
  }
243
251
  })
244
-
245
- // 2. Position the destination (winner) node for the game
246
252
  if (
247
253
  game.winnerDestination &&
248
254
  typeof game.winnerDestination === 'object' &&
@@ -261,8 +267,6 @@ const computeBracketLayout = () => {
261
267
  }
262
268
  }
263
269
  }
264
-
265
- // 3. Position the loser destination node for the game
266
270
  if (
267
271
  game.loserDestination &&
268
272
  typeof game.loserDestination === 'object' &&
@@ -282,8 +286,6 @@ const computeBracketLayout = () => {
282
286
  }
283
287
  }
284
288
  })
285
-
286
- // Add winnerPlace and loserPlace nodes from each round
287
289
  props.bracketData.round.forEach((round) => {
288
290
  if (Array.isArray(round.winnerPlace)) {
289
291
  round.winnerPlace.forEach((place) => {
@@ -308,8 +310,23 @@ const computeBracketLayout = () => {
308
310
  })
309
311
  }
310
312
  })
311
-
312
313
  nodeGridPositions.value = newPositions
314
+
315
+ // Cache layout values
316
+ const uniqueColumns = [
317
+ ...new Set(props.bracketData.round.map((r) => r.columnNo)),
318
+ ]
319
+ const numColumns = uniqueColumns.length
320
+ let maxRow = 0
321
+ Object.values(newPositions).forEach(({ row }) => {
322
+ if (row > maxRow) maxRow = row
323
+ })
324
+ layoutCache.value.numColumns = numColumns
325
+ layoutCache.value.maxRow = maxRow
326
+ layoutCache.value.contentWidth =
327
+ 50 + (numColumns - 1) * props.hGap + nodeWidth + 50
328
+ layoutCache.value.contentHeight =
329
+ 50 + (maxRow - 1) * props.vGap + nodeHeight + 50
313
330
  }
314
331
 
315
332
  const descriptiveRoundName = (round) => {
@@ -442,6 +459,136 @@ const getTeamPath = (teamId) => {
442
459
  }
443
460
  }
444
461
 
462
+ const normalizeBracketData = (bracketData) => {
463
+ const minCol = bracketData.minColumn
464
+ ? Number(bracketData.minColumn)
465
+ : Math.min(...bracketData.round.map((r) => Number(r.columnNo)))
466
+ const minRow = bracketData.minRow
467
+ ? Number(bracketData.minRow)
468
+ : Math.min(
469
+ ...bracketData.round
470
+ .flatMap((r) =>
471
+ (r.game || []).flatMap((g) =>
472
+ (g.team || []).map((t) => Number(t.position?.rowNo) || 0)
473
+ )
474
+ )
475
+ .filter((n) => n > 0)
476
+ )
477
+
478
+ // Normalize all positions so columns/rows start from 1
479
+ const normRounds = []
480
+ for (const round of bracketData.round) {
481
+ const normGames = []
482
+ if (Array.isArray(round.game)) {
483
+ for (const game of round.game) {
484
+ const normTeams = []
485
+ if (Array.isArray(game.team)) {
486
+ for (const team of game.team) {
487
+ if (team.position) {
488
+ normTeams.push({
489
+ ...team,
490
+ position: {
491
+ columnNo: Number(team.position.columnNo) - minCol + 1,
492
+ rowNo: Number(team.position.rowNo) - minRow + 1,
493
+ },
494
+ })
495
+ } else {
496
+ normTeams.push(team)
497
+ }
498
+ }
499
+ }
500
+ let normWinnerDest = game.winnerDestination
501
+ if (
502
+ normWinnerDest &&
503
+ normWinnerDest.team &&
504
+ normWinnerDest.team.position
505
+ ) {
506
+ normWinnerDest = {
507
+ ...normWinnerDest,
508
+ team: {
509
+ ...normWinnerDest.team,
510
+ position: {
511
+ columnNo:
512
+ Number(normWinnerDest.team.position.columnNo) - minCol + 1,
513
+ rowNo: Number(normWinnerDest.team.position.rowNo) - minRow + 1,
514
+ },
515
+ },
516
+ }
517
+ }
518
+ let normLoserDest = game.loserDestination
519
+ if (
520
+ normLoserDest &&
521
+ normLoserDest.team &&
522
+ normLoserDest.team.position
523
+ ) {
524
+ normLoserDest = {
525
+ ...normLoserDest,
526
+ team: {
527
+ ...normLoserDest.team,
528
+ position: {
529
+ columnNo:
530
+ Number(normLoserDest.team.position.columnNo) - minCol + 1,
531
+ rowNo: Number(normLoserDest.team.position.rowNo) - minRow + 1,
532
+ },
533
+ },
534
+ }
535
+ }
536
+ normGames.push({
537
+ ...game,
538
+ team: normTeams,
539
+ winnerDestination: normWinnerDest,
540
+ loserDestination: normLoserDest,
541
+ })
542
+ }
543
+ }
544
+ // Normalize winnerPlace/loserPlace
545
+ const normWinnerPlace = []
546
+ if (Array.isArray(round.winnerPlace)) {
547
+ for (const place of round.winnerPlace) {
548
+ if (place.position) {
549
+ normWinnerPlace.push({
550
+ ...place,
551
+ position: {
552
+ columnNo: Number(place.position.columnNo) - minCol + 1,
553
+ rowNo: Number(place.position.rowNo) - minRow + 1,
554
+ },
555
+ })
556
+ } else {
557
+ normWinnerPlace.push(place)
558
+ }
559
+ }
560
+ }
561
+ const normLoserPlace = []
562
+ if (Array.isArray(round.loserPlace)) {
563
+ for (const place of round.loserPlace) {
564
+ if (place.position) {
565
+ normLoserPlace.push({
566
+ ...place,
567
+ position: {
568
+ columnNo: Number(place.position.columnNo) - minCol + 1,
569
+ rowNo: Number(place.position.rowNo) - minRow + 1,
570
+ },
571
+ })
572
+ } else {
573
+ normLoserPlace.push(place)
574
+ }
575
+ }
576
+ }
577
+ normRounds.push({
578
+ ...round,
579
+ columnNo: Number(round.columnNo) - minCol + 1,
580
+ roundName: round.roundName,
581
+ game: normGames,
582
+ winnerPlace: normWinnerPlace,
583
+ loserPlace: normLoserPlace,
584
+ })
585
+ }
586
+ return {
587
+ ...bracketData,
588
+ round: normRounds,
589
+ }
590
+ }
591
+
445
592
  const openExpandView = () => {
446
593
  emit('open-expand')
447
594
  }
@@ -656,11 +803,8 @@ const renderBracket = () => {
656
803
  nodeGroups
657
804
  .append('text')
658
805
  .attr('class', 'node-label')
659
- .attr('x', (d) => d.x + 54) // Move label right only for seed teams
806
+ .attr('x', (d) => d.x + (seedTeamIds.includes(d.id) ? 50 : 10)) // Move label right only for seed teams
660
807
  .attr('y', (d) => d.y + nodeHeight / 2 + 5)
661
- .attr('text-anchor', (d) =>
662
- seedTeamIds.includes(d.id) ? 'start' : 'middle'
663
- )
664
808
  .text((d) => d.label)
665
809
 
666
810
  // Add '-' text to the right side for seed teams
@@ -700,44 +844,131 @@ const renderBracket = () => {
700
844
  const destPos = getNodePosition(destId)
701
845
 
702
846
  if (pos1 && pos2 && destPos) {
703
- // Horizontally, it should be halfway between the end of the source node and the start of the destination node.
704
- const x = pos1.x + nodeWidth / 2
705
- // Vertically, it should be centered between the two source nodes.
706
- const y = (pos1.y + nodeHeight + pos2.y) / 2
847
+ // Use the leftmost node's x for alignment
848
+ const nodeX1 = pos1.x
849
+ const nodeX2 = pos2.x
850
+ const nodeY1 = pos1.y
851
+ const nodeY2 = pos2.y
852
+
853
+ const startX = Math.min(nodeX1, nodeX2)
854
+ const topY = Math.min(nodeY1, nodeY2)
855
+ const bottomY = Math.max(nodeY1, nodeY2) + nodeHeight
856
+ // Move details upward by 12px so it doesn't touch the node
857
+ const centerY = (topY + bottomY) / 2 - 12
858
+
859
+ const detailsGroup = g.append('g').attr('transform', `translate(${startX}, ${centerY})`)
707
860
 
708
- const dateGroup = g.append('g').attr('transform', `translate(${x}, ${y})`)
861
+ // Remove the white background rect (unwanted)
862
+ // detailsGroup.append('rect') ... (removed)
709
863
 
710
- // Add the date text first
711
- dateGroup
864
+ // Add the date text
865
+ detailsGroup
712
866
  .append('text')
713
867
  .attr('class', 'match-date text-overline-xs text-description')
714
- .attr('x', 30) // Position text to end just left of center
715
- .attr('y', 2)
716
- .attr('text-anchor', 'end') // Anchor text from the end
868
+ .attr('x', 0)
869
+ .attr('y', -6)
870
+ .attr('text-anchor', 'start')
717
871
  .attr('dominant-baseline', 'middle')
718
872
  .text(game.date)
719
873
 
720
- // Add the icon after the text
721
- const locationIcon = dateGroup
874
+ // Add venue text below date text from the 'venue' key
875
+ if (game.venue) {
876
+ // We'll use a <foreignObject> with a div, and check for overflow after rendering
877
+ const venueFO = detailsGroup
878
+ .append('foreignObject')
879
+ .attr('x', 0)
880
+ .attr('y', 2)
881
+ .attr('width', 200)
882
+ .attr('height', 30)
883
+ .append('xhtml:div')
884
+ .attr('class', 'text-body-xxs text-description match-venue venue-ellipsis')
885
+ .style('word-break', 'break-word')
886
+ .style('white-space', 'normal')
887
+ .style('text-align', 'start')
888
+ .style('width', '100%')
889
+ .style('overflow', 'hidden')
890
+ .style('display', '-webkit-box')
891
+ .style('-webkit-line-clamp', 2)
892
+ .style('-webkit-box-orient', 'vertical')
893
+ .text(game.venue);
894
+
895
+ // After nextTick, check for vertical overflow and add tooltip if needed
896
+ nextTick(() => {
897
+ // venueFO is a D3 selection, so .node() gives us the div
898
+ const div = venueFO.node();
899
+ if (div && div.scrollHeight > div.clientHeight + 1) {
900
+ // Add ellipsis via CSS (already set), and tooltip on hover
901
+ d3.select(div)
902
+ .on('mouseover', function (event) {
903
+ const rect = this.getBoundingClientRect()
904
+ const svgRect = svgRef.value.getBoundingClientRect()
905
+ tooltipState.value = {
906
+ visible: true,
907
+ x: rect.left - svgRect.left + rect.width / 2,
908
+ y: rect.top - svgRect.top - 40,
909
+ text: game.venue,
910
+ }
911
+ })
912
+ .on('mouseout', function () {
913
+ tooltipState.value = { ...tooltipState.value, visible: false }
914
+ })
915
+ }
916
+ });
917
+ }
918
+
919
+ const locationIcon = detailsGroup
722
920
  .append('image')
723
921
  .attr('href', '/icons/field-location.svg')
724
- .attr('x', 95)
725
- .attr('y', -12)
922
+ .attr('x', 200)
923
+ .attr('y', -8)
726
924
  .attr('width', 24)
727
925
  .attr('height', 24)
728
926
  .attr('class', 'location-icon')
729
927
  .style('cursor', 'pointer')
730
928
 
929
+ const scheduleIcon = detailsGroup
930
+ .append('image')
931
+ .attr('href', '/icons/scoreboard.svg')
932
+ .attr('x', 240)
933
+ .attr('y', -8)
934
+ .attr('width', 24)
935
+ .attr('height', 24)
936
+ .attr('class', 'info-icon')
937
+ .style('cursor', 'pointer')
938
+
731
939
  locationIcon.on('click', function (event) {
732
- const iconRect = this.getBoundingClientRect()
733
- const svgRect = svgRef.value.getBoundingClientRect()
734
- tooltipState.value = {
735
- visible: true,
736
- x: iconRect.left - svgRect.left + iconRect.width / 2,
737
- y: iconRect.top - svgRect.top - 40,
738
- text: game.location || 'Dallas, TX',
940
+ if ($screen.value.isMobile) {
941
+ locationSheet.value.push({ open: true, height: 150 })
942
+ } else {
943
+ const iconRect = this.getBoundingClientRect()
944
+ const svgRect = svgRef.value.getBoundingClientRect()
945
+ tooltipState.value = {
946
+ visible: true,
947
+ x: iconRect.left - svgRect.left + iconRect.width / 2,
948
+ y: iconRect.top - svgRect.top - 40,
949
+ text: game.location || 'Dallas, TX',
950
+ }
739
951
  }
740
952
  })
953
+
954
+ // Tooltip on hover for schedule icon
955
+ scheduleIcon
956
+ .on('mouseover', function (event) {
957
+ const iconRect = this.getBoundingClientRect()
958
+ const svgRect = svgRef.value.getBoundingClientRect()
959
+ tooltipState.value = {
960
+ visible: true,
961
+ x: iconRect.left - svgRect.left + iconRect.width / 2,
962
+ y: iconRect.top - svgRect.top - 40,
963
+ text: props.viewMatchupDetailText,
964
+ }
965
+ })
966
+ .on('mouseout', function () {
967
+ tooltipState.value = { ...tooltipState.value, visible: false }
968
+ })
969
+ .on('click', function (event) {
970
+ emit('schedule-click', game)
971
+ })
741
972
  }
742
973
  })
743
974
 
@@ -765,6 +996,39 @@ const renderBracket = () => {
765
996
  (winnerNodePosition.x !== 0 || winnerNodePosition.y !== 0)
766
997
  ) {
767
998
  const { x: mx, y: my } = winnerNodePosition
999
+
1000
+ let labelPlaced = false
1001
+
1002
+ const sourcePositions = (game.team || [])
1003
+ .map((source) => {
1004
+ if (
1005
+ typeof source === 'object' &&
1006
+ source.teamName &&
1007
+ source.position
1008
+ ) {
1009
+ const id = `${source.teamName}_${source.position.columnNo}_${source.position.rowNo}`
1010
+ return getNodePosition(id)
1011
+ }
1012
+ return null
1013
+ })
1014
+ .filter(Boolean)
1015
+
1016
+ let triX = 0, triY = 0
1017
+ if (sourcePositions.length === 2) {
1018
+ if (mx > sourcePositions[0].x) {
1019
+ // Winner to the right
1020
+ triX = (sourcePositions[0].x + nodeWidth + mx - edgeMargin) / 2
1021
+ } else {
1022
+ // Winner to the left
1023
+ triX = (sourcePositions[0].x - edgeMargin + mx + nodeWidth) / 2
1024
+ }
1025
+ // Always use the vertical center of the destination node
1026
+ triY = my + nodeHeight / 2
1027
+ } else if (sourcePositions.length === 1) {
1028
+ triX = (sourcePositions[0].x + mx) / 2
1029
+ triY = my + nodeHeight / 2
1030
+ }
1031
+
768
1032
  game.team.forEach((source) => {
769
1033
  let sourceNodeId
770
1034
  if (
@@ -807,6 +1071,47 @@ const renderBracket = () => {
807
1071
  .attr('d', path.toString())
808
1072
  .attr('stroke-linecap', 'round')
809
1073
  })
1074
+
1075
+ // Place the gameAliasName at the trisection point (only once per edge group)
1076
+ if (game.gameAliasName && !labelPlaced && sourcePositions.length) {
1077
+ const aliasText = game.gameAliasName.replace(/-/g, '').trim();
1078
+ const padding = 6 // px, padding inside the square
1079
+ const tempText = g.append('text')
1080
+ .attr('class', 'text-caption-xs game-number')
1081
+ .attr('x', triX)
1082
+ .attr('y', triY)
1083
+ .attr('text-anchor', 'middle')
1084
+ .attr('dominant-baseline', 'middle')
1085
+ .style('visibility', 'hidden')
1086
+ .text(aliasText)
1087
+ const bbox = tempText.node().getBBox()
1088
+ tempText.remove()
1089
+ const size = Math.max(bbox.width, bbox.height) + padding
1090
+
1091
+ // Draw white square background with edge color border
1092
+ g.append('rect')
1093
+ .attr('x', triX - size / 2)
1094
+ .attr('y', triY - size / 2)
1095
+ .attr('width', size)
1096
+ .attr('height', size)
1097
+ .attr('rx', 4)
1098
+ .attr('fill', '#fff')
1099
+ .attr('stroke', getComputedStyle(document.documentElement)
1100
+ .getPropertyValue('--q-color-neutral-4') || '#b9e0ff')
1101
+ .attr('stroke-width', 4)
1102
+ .attr('class', 'edge-alias-bg')
1103
+
1104
+ // Draw the label text
1105
+ g.append('text')
1106
+ .attr('class', 'text-caption-xs text-description game-alias-label')
1107
+ .attr('x', triX)
1108
+ .attr('y', triY)
1109
+ .attr('text-anchor', 'middle')
1110
+ .attr('dominant-baseline', 'middle')
1111
+ .style('pointer-events', 'none')
1112
+ .text(aliasText)
1113
+ labelPlaced = true
1114
+ }
810
1115
  }
811
1116
  }
812
1117
 
@@ -831,6 +1136,32 @@ const renderBracket = () => {
831
1136
  (loserNodePosition.x !== 0 || loserNodePosition.y !== 0)
832
1137
  ) {
833
1138
  const { x: lx, y: ly } = loserNodePosition
1139
+
1140
+ let labelPlaced = false
1141
+
1142
+ const sourcePositions = (game.team || [])
1143
+ .map((source) => {
1144
+ if (
1145
+ typeof source === 'object' &&
1146
+ source.teamName &&
1147
+ source.position
1148
+ ) {
1149
+ const id = `${source.teamName}_${source.position.columnNo}_${source.position.rowNo}`
1150
+ return getNodePosition(id)
1151
+ }
1152
+ return null
1153
+ })
1154
+ .filter(Boolean)
1155
+
1156
+ let triX = 0, triY = 0
1157
+ if (sourcePositions.length === 2) {
1158
+ triX = (sourcePositions[0].x - edgeMargin + lx + nodeWidth) / 2
1159
+ triY = ly + nodeHeight / 2
1160
+ } else if (sourcePositions.length === 1) {
1161
+ triX = (sourcePositions[0].x + lx) / 2
1162
+ triY = ly + nodeHeight / 2
1163
+ }
1164
+
834
1165
  game.team.forEach((source) => {
835
1166
  let sourceNodeId
836
1167
  if (
@@ -861,39 +1192,141 @@ const renderBracket = () => {
861
1192
  .attr('d', path.toString())
862
1193
  .attr('stroke-linecap', 'round') // Changed to rounded end
863
1194
  })
1195
+
1196
+ // Place the gameAliasName at the trisection point (only once per edge group)
1197
+ if (game.gameAliasName && !labelPlaced && sourcePositions.length) {
1198
+ // Remove hyphens from gameAliasName
1199
+ const aliasText = game.gameAliasName.replace(/-/g, '').trim();
1200
+ // Calculate text size for the background rect
1201
+ const fontSize = 13 // px, matches .edge-alias-label font-size
1202
+ const padding = 6 // px, padding inside the square
1203
+ // Temporary text element to measure width
1204
+ const tempText = g.append('text')
1205
+ .attr('class', 'text-caption-xs text-description')
1206
+ .attr('x', triX)
1207
+ .attr('y', triY)
1208
+ .attr('text-anchor', 'middle')
1209
+ .attr('dominant-baseline', 'middle')
1210
+ .style('visibility', 'hidden')
1211
+ .text(aliasText)
1212
+ const bbox = tempText.node().getBBox()
1213
+ tempText.remove()
1214
+ const size = Math.max(bbox.width, bbox.height) + padding
1215
+
1216
+ // Draw white square background with edge color border
1217
+ g.append('rect')
1218
+ .attr('x', triX - size / 2)
1219
+ .attr('y', triY - size / 2)
1220
+ .attr('width', size)
1221
+ .attr('height', size)
1222
+ .attr('rx', 5)
1223
+ .attr('fill', '#fff')
1224
+ .attr('stroke', getComputedStyle(document.documentElement)
1225
+ .getPropertyValue('--q-color-neutral-4') || '#b9e0ff')
1226
+ .attr('stroke-width', 4)
1227
+ .attr('class', 'edge-alias-bg')
1228
+
1229
+ // Draw the label text
1230
+ g.append('text')
1231
+ .attr('class', 'text-caption-xs text-description game-alias-label')
1232
+ .attr('x', triX)
1233
+ .attr('y', triY)
1234
+ .attr('text-anchor', 'middle')
1235
+ .attr('dominant-baseline', 'middle')
1236
+ .style('pointer-events', 'none')
1237
+ .text(aliasText)
1238
+ labelPlaced = true
1239
+ }
864
1240
  }
865
- }
866
- })
1241
+ } // <-- FIX: close the LOSER edges block here
1242
+
1243
+ }) // <-- close games.value.forEach
867
1244
 
868
1245
  // After all edges are drawn, move highlighted edges to top
869
1246
  nextTick(() => {
870
1247
  const gEl = gRef.value
871
1248
  if (gEl) {
1249
+ // Move highlighted edges to top
872
1250
  const highlightedEdges = gEl.querySelectorAll('.edge-path.highlighted')
873
1251
  highlightedEdges.forEach((el) => {
874
1252
  gEl.appendChild(el)
875
1253
  })
1254
+ // Move game-alias boxes (rects and texts) to top after edges
1255
+ const aliasRects = gEl.querySelectorAll('.edge-alias-bg')
1256
+ const aliasLabels = gEl.querySelectorAll('.game-alias-label')
1257
+ aliasRects.forEach((el) => {
1258
+ gEl.appendChild(el)
1259
+ })
1260
+ aliasLabels.forEach((el) => {
1261
+ gEl.appendChild(el)
1262
+ })
876
1263
  }
877
1264
  })
878
1265
  }
879
1266
 
880
- const scrollToBracketSide = (side) => {
881
- if (!props.bracketData || !props.bracketData.round) return
1267
+ function resetZoom() {
1268
+ const svg = d3.select(svgRef.value)
1269
+ if (zoomRef.value && svgRef.value) {
1270
+ const svgElement = svgRef.value
1271
+ const { width, height } = svgElement.getBoundingClientRect()
1272
+ const centerY = height / 2
1273
+
1274
+ const k = 1
1275
+ const contentWidth = layoutCache.value.contentWidth
1276
+ const contentHeight = layoutCache.value.contentHeight
1277
+ const viewportWidth = svgElement.clientWidth
1278
+ const viewportHeight = svgElement.clientHeight
1279
+ const minTx = viewportWidth - contentWidth * k
1280
+ const maxTx = 0
1281
+ const minTy = viewportHeight - contentHeight * k
1282
+ const maxTy = 0
1283
+
1284
+ // Center on selected team if available, else round 1
1285
+ let tx, ty
1286
+ let targetX = null,
1287
+ targetY = null
1288
+ if (selectedTeamId.value) {
1289
+ const pos = getNodePosition(selectedTeamId.value)
1290
+ targetX = pos.x + nodeWidth / 2
1291
+ targetY = pos.y + nodeHeight / 2
1292
+ } else {
1293
+ const firstRound = props.bracketData.round?.[0]
1294
+ if (firstRound) {
1295
+ targetX = (firstRound.columnNo - 1) * props.hGap + props.hGap / 2
1296
+ targetY = centerY
1297
+ } else {
1298
+ targetX = viewportWidth / 2
1299
+ targetY = centerY
1300
+ }
1301
+ }
1302
+ tx = viewportWidth / 2 - targetX * k
1303
+ ty = viewportHeight / 2 - targetY * k
1304
+ tx = minTx > 0 ? minTx / 2 : Math.max(minTx, Math.min(maxTx, tx))
1305
+ ty = minTy > 0 ? minTy / 2 : Math.max(minTy, Math.min(maxTy, ty))
1306
+ const clampedTransform = d3.zoomIdentity.translate(tx, ty).scale(k)
1307
+ svg
1308
+ .transition()
1309
+ .duration(400)
1310
+ .ease(d3.easeCubic)
1311
+ .call(zoomRef.value.transform, clampedTransform)
1312
+ d3.select(gRef.value).attr('transform', clampedTransform)
1313
+ currentTransform.value = clampedTransform
1314
+ }
1315
+ }
882
1316
 
883
- const firstRound = props.bracketData.round[0]
1317
+ const scrollToBracketSide = (side) => {
1318
+ if (!normalizedBracketData.value || !normalizedBracketData.value.round) return
1319
+ const firstRound = normalizedBracketData.value.round[0]
884
1320
  if (!firstRound || !firstRound.game || firstRound.game.length === 0) return
885
-
886
1321
  const firstMatch = firstRound.game[0]
887
1322
  let targetColumn = null
888
-
889
1323
  if (side === 'winner' && firstMatch.winnerDestination) {
890
1324
  targetColumn = firstMatch.winnerDestination.team.position.columnNo
891
1325
  } else if (side === 'loser' && firstMatch.loserDestination) {
892
1326
  targetColumn = firstMatch.loserDestination.team.position.columnNo
893
1327
  }
894
-
895
1328
  if (targetColumn) {
896
- const targetRound = props.bracketData.round.find(
1329
+ const targetRound = normalizedBracketData.value.round.find(
897
1330
  (r) => r.columnNo === targetColumn
898
1331
  )
899
1332
  if (targetRound) {
@@ -904,41 +1337,26 @@ const scrollToBracketSide = (side) => {
904
1337
 
905
1338
  const scrollToRound = (round) => {
906
1339
  activeRound.value = { roundNo: round.roundNo, columnNo: round.columnNo }
907
- openRoundMenu.value = false // Close the menu on selection
1340
+ openRoundMenu.value = false
908
1341
  const svg = d3.select(svgRef.value)
909
1342
  const zoomBehavior = zoomRef.value
910
1343
  if (!zoomBehavior || !svgRef.value) return
911
1344
 
912
- // Calculate the x-coordinate of the target round's center.
913
- const roundX = (round.columnNo - 1) * props.hGap + props.hGap / 2
1345
+ const normalizedRound = normalizedBracketData.value.round
914
1346
 
915
- // Get the current viewport width.
916
- const viewportWidth = svgRef.value.clientWidth
1347
+ const mainRound = normalizedRound.find(
1348
+ (r) => r.roundName == round.roundName && r.roundNo == round.roundNo
1349
+ )
917
1350
 
918
- // Get the LIVE transform directly from the SVG node for accuracy.
919
- // This is the key fix: currentTransform.value can be stale during transitions.
1351
+ const roundX = (mainRound.columnNo - 1) * props.hGap + props.hGap / 2
1352
+ const viewportWidth = svgRef.value.clientWidth
920
1353
  const liveTransform = d3.zoomTransform(svg.node())
921
1354
  const { k: currentK, y: currentY } = liveTransform
922
-
923
- // Determine content dimensions.
924
- const uniqueColumns = [
925
- ...new Set(props.bracketData.round.map((r) => r.columnNo)),
926
- ]
927
- const numColumns = uniqueColumns.length
928
- const contentWidth = 50 + (numColumns - 1) * props.hGap + nodeWidth + 50
929
-
930
- // Calculate the new x-translate to center the round's center in the viewport.
931
1355
  const targetX = viewportWidth / 2 - roundX * currentK
932
-
933
1356
  const clampedX = targetX
934
-
935
- // Create the new transform, keeping the current scale and y-pan.
936
-
937
1357
  const newTransform = d3.zoomIdentity
938
1358
  .translate(clampedX, currentY)
939
1359
  .scale(currentK)
940
-
941
- // Apply the new transform with a smooth transition.
942
1360
  svg.transition().duration(750).call(zoomBehavior.transform, newTransform)
943
1361
  }
944
1362
 
@@ -953,10 +1371,20 @@ const setSelectedTeamId = (teamId) => {
953
1371
  const team = toolbarTeams.value.find((t) => t.id === teamId)
954
1372
  if (team) {
955
1373
  selectedTeamId.value = team.id
1374
+ // Reset zoom to initial scale and center
1375
+ resetZoom()
956
1376
  }
957
1377
  openMenu.value = false
958
1378
  }
959
1379
 
1380
+ const toggleBracketSideInSheet = (side) => {
1381
+ tempSelectedBracketSide.value = side
1382
+ }
1383
+
1384
+ const toggleRoundInSheet = (round) => {
1385
+ tempSelectedRoundId.value = `${round.roundNo}-${round.roundName}`
1386
+ }
1387
+
960
1388
  const toggleTeamInSheet = (teamId) => {
961
1389
  if (teamId === tempSelectedTeamId.value) {
962
1390
  tempSelectedTeamId.value = ''
@@ -965,41 +1393,59 @@ const toggleTeamInSheet = (teamId) => {
965
1393
  tempSelectedTeamId.value = teamId
966
1394
  }
967
1395
 
968
- const toggleBracketSideInSheet = (side) => {
969
- tempSelectedBracketSide.value = side
970
- }
971
-
972
- const toggleRoundInSheet = (round) => {
973
- tempSelectedRoundId.value = `${round.roundNo}-${round.columnNo}`
974
- }
975
-
976
1396
  const zoomIn = () => {
977
1397
  const svg = d3.select(svgRef.value)
978
- if (zoomRef.value) {
1398
+ if (zoomRef.value && svgRef.value) {
1399
+ const svgElement = svgRef.value
1400
+ const { width, height } = svgElement.getBoundingClientRect()
1401
+ const centerX = width / 2
1402
+ const centerY = height / 2
1403
+
979
1404
  // Increase scale by 20%
980
- const newScale = Math.min((currentTransform.value.k || 1) * 1.2, 2.5)
981
- const newTransform = d3.zoomIdentity
982
- .scale(newScale)
983
- .translate(
984
- currentTransform.value.x / newScale,
985
- currentTransform.value.y / newScale
986
- )
987
- svg.transition().duration(200).call(zoomRef.value.transform, newTransform)
1405
+ const prevScale = currentTransform.value.k || 1
1406
+ const newScale = Math.min(prevScale * 1.2, 2.5)
1407
+
1408
+ // Calculate new translation to keep center in view
1409
+ const newX =
1410
+ centerX - (centerX - currentTransform.value.x) * (newScale / prevScale)
1411
+ const newY =
1412
+ centerY - (centerY - currentTransform.value.y) * (newScale / prevScale)
1413
+
1414
+ const newTransform = d3.zoomIdentity.translate(newX, newY).scale(newScale)
1415
+
1416
+ svg
1417
+ .transition()
1418
+ .duration(200)
1419
+ .ease(d3.easeCubic)
1420
+ .call(zoomRef.value.transform, newTransform)
988
1421
  }
989
1422
  }
990
1423
 
991
1424
  const zoomOut = () => {
992
1425
  const svg = d3.select(svgRef.value)
993
- if (zoomRef.value) {
1426
+ if (zoomRef.value && svgRef.value) {
1427
+ const svgElement = svgRef.value
1428
+ const { width, height } = svgElement.getBoundingClientRect()
1429
+ const centerX = width / 2
1430
+ const centerY = height / 2
1431
+
994
1432
  // Decrease scale by 20%
995
- const newScale = Math.max((currentTransform.value.k || 1) / 1.2, 0.5)
996
- const newTransform = d3.zoomIdentity
997
- .scale(newScale)
998
- .translate(
999
- currentTransform.value.x / newScale,
1000
- currentTransform.value.y / newScale
1001
- )
1002
- svg.transition().duration(200).call(zoomRef.value.transform, newTransform)
1433
+ const prevScale = currentTransform.value.k || 1
1434
+ const newScale = Math.max(prevScale / 1.2, 0.5)
1435
+
1436
+ // Calculate new translation to keep center in view
1437
+ const newX =
1438
+ centerX - (centerX - currentTransform.value.x) * (newScale / prevScale)
1439
+ const newY =
1440
+ centerY - (centerY - currentTransform.value.y) * (newScale / prevScale)
1441
+
1442
+ const newTransform = d3.zoomIdentity.translate(newX, newY).scale(newScale)
1443
+
1444
+ svg
1445
+ .transition()
1446
+ .duration(200)
1447
+ .ease(d3.easeCubic)
1448
+ .call(zoomRef.value.transform, newTransform)
1003
1449
  }
1004
1450
  }
1005
1451
 
@@ -1013,6 +1459,7 @@ watch(
1013
1459
  reinitZoomAndLayout()
1014
1460
  }
1015
1461
  )
1462
+
1016
1463
  watch(
1017
1464
  () => props.vGap,
1018
1465
  () => {
@@ -1060,7 +1507,7 @@ onMounted(() => {
1060
1507
 
1061
1508
  // Determine content dimensions
1062
1509
  const uniqueColumns = [
1063
- ...new Set(props.bracketData.round.map((r) => r.columnNo)),
1510
+ ...new Set(normalizedBracketData.value.round.map((r) => r.columnNo)),
1064
1511
  ]
1065
1512
  const numColumns = uniqueColumns.length
1066
1513
  const contentWidth = 50 + (numColumns + 2) * props.hGap + nodeWidth + 50
@@ -1075,35 +1522,55 @@ onMounted(() => {
1075
1522
  const zoomBehavior = d3
1076
1523
  .zoom()
1077
1524
  .scaleExtent([0.5, 2.5])
1078
- // Filter events to prevent page scroll on wheel events (pan/zoom)
1079
- // while allowing other events like mouse clicks to pass through.
1525
+ // Custom filter to intercept wheel events and always zoom centered
1080
1526
  .filter((event) => {
1081
1527
  if (event.type === 'wheel') {
1082
1528
  event.preventDefault()
1529
+ // Manually handle wheel zoom centered on SVG
1530
+ const svgElement = svgRef.value
1531
+ const { width, height } = svgElement.getBoundingClientRect()
1532
+ const centerX = width / 2
1533
+ const centerY = height / 2
1534
+ const prevScale = currentTransform.value.k || 1
1535
+ let newScale = prevScale
1536
+ // D3 uses event.deltaY for wheel direction
1537
+ if (event.deltaY < 0) {
1538
+ newScale = Math.min(prevScale * 1.2, 2.5)
1539
+ } else {
1540
+ newScale = Math.max(prevScale / 1.2, 0.5)
1541
+ }
1542
+ // Calculate new translation to keep center in view
1543
+ const newX =
1544
+ centerX -
1545
+ (centerX - currentTransform.value.x) * (newScale / prevScale)
1546
+ const newY =
1547
+ centerY -
1548
+ (centerY - currentTransform.value.y) * (newScale / prevScale)
1549
+ const newTransform = d3.zoomIdentity
1550
+ .translate(newX, newY)
1551
+ .scale(newScale)
1552
+ d3.select(svgRef.value)
1553
+ .transition()
1554
+ .duration(200)
1555
+ .ease(d3.easeCubic)
1556
+ .call(zoomBehavior.transform, newTransform)
1557
+ return false // Prevent default D3 wheel zoom
1083
1558
  }
1084
- // Allow click-and-drag panning
1085
1559
  return !event.ctrlKey || event.type === 'wheel'
1086
1560
  })
1087
- // We clamp manually in the zoom event instead of using translateExtent
1088
1561
  .on('zoom', (event) => {
1089
1562
  let { transform } = event
1090
1563
  const { k } = transform
1091
- tooltipState.value.visible = false // Hide tooltip on zoom/pan
1092
-
1093
- // Calculate boundaries for clamping.
1564
+ tooltipState.value.visible = false
1094
1565
  const minTx = viewportWidth - contentWidth * k
1095
1566
  const maxTx = 0
1096
1567
  const minTy = viewportHeight - contentHeight * k
1097
1568
  const maxTy = 0
1098
-
1099
1569
  const tx =
1100
1570
  minTx > 0 ? minTx / 2 : Math.max(minTx, Math.min(maxTx, transform.x))
1101
1571
  const ty =
1102
1572
  minTy > 0 ? minTy / 2 : Math.max(minTy, Math.min(maxTy, transform.y))
1103
-
1104
- // Create a new clamped transform
1105
1573
  const clampedTransform = d3.zoomIdentity.translate(tx, ty).scale(k)
1106
-
1107
1574
  g.attr('transform', clampedTransform)
1108
1575
  currentTransform.value = clampedTransform
1109
1576
  })
@@ -1111,19 +1578,40 @@ onMounted(() => {
1111
1578
  svg.call(zoomBehavior)
1112
1579
  zoomRef.value = zoomBehavior
1113
1580
 
1114
- // Focus on round 1 when bracket loads
1115
- const firstRound = props.bracketData.round?.[0]
1581
+ // Focus on first normalized round when bracket loads
1582
+ const firstRound = normalizedBracketData.value.round?.[0]
1116
1583
  if (firstRound) scrollToRound(firstRound)
1117
1584
 
1118
1585
  renderBracket()
1119
1586
 
1120
- // Add window resize listener
1121
1587
  window.addEventListener('resize', reinitZoomAndLayout)
1122
1588
  })
1123
1589
 
1124
1590
  onUnmounted(() => {
1125
1591
  window.removeEventListener('resize', reinitZoomAndLayout)
1126
- document.removeEventListener('click', () => {})
1592
+ })
1593
+
1594
+ watch(
1595
+ () => props.bracketData,
1596
+ () => {
1597
+ // Reset selections on bracket data change
1598
+ selectedTeamId.value = ''
1599
+ tempSelectedTeamId.value = ''
1600
+ tempSelectedRoundId.value = ''
1601
+ tempSelectedBracketSide.value = ''
1602
+ bracketSheet.value = []
1603
+ roundSheet.value = []
1604
+ computeBracketLayout()
1605
+ renderBracket()
1606
+ },
1607
+ { deep: true }
1608
+ )
1609
+
1610
+ watch(currentTransform, (newVal) => {
1611
+ // Only update on explicit user zoom/pan, not on initial mount
1612
+ if (gRef.value) {
1613
+ d3.select(gRef.value).attr('transform', newVal)
1614
+ }
1127
1615
  })
1128
1616
  </script>
1129
1617
 
@@ -1152,20 +1640,20 @@ onUnmounted(() => {
1152
1640
  :ripple="false"
1153
1641
  size="sm"
1154
1642
  @click="selectedTeamName = ''"
1155
- ></UBtnStd>
1643
+ />
1156
1644
  </div>
1157
1645
  </div>
1158
1646
  </div>
1159
1647
  <div class="sticky-round-labels">
1160
1648
  <div
1161
1649
  v-for="(round, i) in bracketData.round"
1162
- :key="`${round.roundNo}-${round.columnNo}`"
1163
1650
  class="round-label text-body-sm"
1164
1651
  :class="{
1165
1652
  'active-round':
1166
1653
  activeRound.roundNo === round.roundNo &&
1167
1654
  activeRound.columnNo === round.columnNo,
1168
1655
  }"
1656
+ :key="`${round.roundNo}-${round.columnNo}`"
1169
1657
  :style="roundLabelStyles[i]"
1170
1658
  @click="scrollToRound(round)"
1171
1659
  >
@@ -1187,7 +1675,7 @@ onUnmounted(() => {
1187
1675
  width="24"
1188
1676
  >
1189
1677
  <rect fill="#F5F7F9" height="24" width="24" x="0" y="0" />
1190
- <circle cx="12" cy="12" fill="#d3d3d3" r="1.5" class="dot" />
1678
+ <circle class="dot" cx="12" cy="12" fill="#d3d3d3" r="1.5" />
1191
1679
  </pattern>
1192
1680
  </defs>
1193
1681
  <rect
@@ -1217,27 +1705,27 @@ onUnmounted(() => {
1217
1705
  >
1218
1706
  <UBtnIcon
1219
1707
  iconClass="fa-kit-duotone fa-zoom-out"
1220
- ariaLabel="Zoom Out"
1221
1708
  :anchor="anchor"
1709
+ ariaLabel="Zoom Out"
1222
1710
  :self="self"
1223
1711
  size="md"
1224
1712
  :tooltip="zoomOutTooltip"
1225
1713
  @click="zoomOut"
1226
- ></UBtnIcon>
1714
+ />
1227
1715
  <UBtnIcon
1228
1716
  iconClass="fa-kit-duotone fa-zoom"
1229
- ariaLabel="Zoom In"
1230
1717
  :anchor="anchor"
1718
+ ariaLabel="Zoom In"
1231
1719
  :self="self"
1232
1720
  size="md"
1233
1721
  :tooltip="zoomInTooltip"
1234
1722
  @click="zoomIn"
1235
- ></UBtnIcon>
1723
+ />
1236
1724
  <UBtnIcon
1237
1725
  v-if="!$screen.isMobile"
1238
1726
  iconClass="fa-kit-duotone fa-bracket"
1239
- ariaLabel="Rounds"
1240
1727
  :anchor="anchor"
1728
+ ariaLabel="Rounds"
1241
1729
  :self="self"
1242
1730
  size="md"
1243
1731
  :tooltip="roundTooltip"
@@ -1258,9 +1746,9 @@ onUnmounted(() => {
1258
1746
  :key="`${round.roundNo}-${round.columnNo}`"
1259
1747
  @click="scrollToRound(round)"
1260
1748
  >
1261
- <q-item-section>{{
1262
- descriptiveRoundName(round)
1263
- }}</q-item-section>
1749
+ <q-item-section>
1750
+ {{ descriptiveRoundName(round) }}
1751
+ </q-item-section>
1264
1752
  </q-item>
1265
1753
  </q-list>
1266
1754
  </q-menu>
@@ -1269,18 +1757,18 @@ onUnmounted(() => {
1269
1757
  <UBtnIcon
1270
1758
  v-if="$screen.isMobile"
1271
1759
  iconClass="fa-kit-duotone fa-bracket"
1272
- ariaLabel="Rounds"
1273
1760
  :anchor="anchor"
1761
+ ariaLabel="Rounds"
1274
1762
  :self="self"
1275
1763
  size="md"
1276
1764
  :tooltip="roundTooltip"
1277
1765
  @click="openRoundSheet"
1278
1766
  />
1279
1767
  <UBtnIcon
1280
- v-if="!$screen.isMobile"
1768
+ v-if="!$screen.isMobile && toolbarTeams.length"
1281
1769
  iconClass="fa-kit-duotone fa-jersey"
1282
- ariaLabel="Select Team"
1283
1770
  :anchor="anchor"
1771
+ ariaLabel="Select Team"
1284
1772
  :self="self"
1285
1773
  size="md"
1286
1774
  :tooltip="teamSelectTooltip"
@@ -1306,7 +1794,7 @@ onUnmounted(() => {
1306
1794
  </template>
1307
1795
  </UBtnIcon>
1308
1796
  <UBtnIcon
1309
- v-if="$screen.isMobile"
1797
+ v-if="$screen.isMobile && toolbarTeams.length"
1310
1798
  iconClass="fa-kit-duotone fa-jersey"
1311
1799
  :anchor="anchor"
1312
1800
  ariaLabel="Select Team"
@@ -1314,8 +1802,7 @@ onUnmounted(() => {
1314
1802
  size="md"
1315
1803
  :tooltip="teamSelectTooltip"
1316
1804
  @click="openTeamOptions"
1317
- >
1318
- </UBtnIcon>
1805
+ />
1319
1806
  <UBtnIcon
1320
1807
  v-if="!props.isExpandedView"
1321
1808
  iconClass="fa-kit-duotone fa-expand"
@@ -1325,7 +1812,7 @@ onUnmounted(() => {
1325
1812
  size="md"
1326
1813
  :tooltip="expandTooltip"
1327
1814
  @click="openExpandView"
1328
- ></UBtnIcon>
1815
+ />
1329
1816
  </div>
1330
1817
  </div>
1331
1818
  </div>
@@ -1395,14 +1882,14 @@ onUnmounted(() => {
1395
1882
  in-sheet
1396
1883
  :label="descriptiveRoundName(round)"
1397
1884
  :selected="
1398
- tempSelectedRoundId === `${round.roundNo}-${round.columnNo}`
1885
+ tempSelectedRoundId === `${round.roundNo}-${round.roundName}`
1399
1886
  "
1400
1887
  @onClick="toggleRoundInSheet(round)"
1401
1888
  >
1402
1889
  <template #trailing_slot>
1403
1890
  <q-icon
1404
1891
  v-if="
1405
- tempSelectedRoundId === `${round.roundNo}-${round.columnNo}`
1892
+ tempSelectedRoundId === `${round.roundNo}-${round.roundName}`
1406
1893
  "
1407
1894
  class="fa-kit-duotone fa-circle-check"
1408
1895
  color="primary"
@@ -1471,6 +1958,26 @@ onUnmounted(() => {
1471
1958
  />
1472
1959
  </template>
1473
1960
  </USheet>
1961
+ <USheet
1962
+ v-model:dialogs="locationSheet"
1963
+ dialog-class="bracket-sheet"
1964
+ :heading="venueSheetHeading"
1965
+ show-action-buttons
1966
+ >
1967
+ <template #content>
1968
+ <div class="text-body-xl text-dark">
1969
+ Dallas, TX
1970
+ </div>
1971
+ </template>
1972
+ <template #action_primary_one>
1973
+ <UBtnStd
1974
+ class="full-width"
1975
+ color="primary"
1976
+ label="Close"
1977
+ @onClick="locationSheet = []"
1978
+ />
1979
+ </template>
1980
+ </USheet>
1474
1981
  </template>
1475
1982
 
1476
1983
  <style scoped lang="sass">
@@ -1509,7 +2016,7 @@ onUnmounted(() => {
1509
2016
  font-weight: normal
1510
2017
  color: $primary
1511
2018
  background: #EEF8FF
1512
- height: 2rem
2019
+ height: $md
1513
2020
  user-select: none
1514
2021
  cursor: pointer
1515
2022
  z-index: 10
@@ -1531,17 +2038,17 @@ svg
1531
2038
  .main-content-dialog
1532
2039
  padding-top: $ba !important
1533
2040
  .q-item
1534
- margin-bottom: $xs
2041
+ margin-bottom: $xs !important
1535
2042
 
1536
2043
  .bracket-toolbar
1537
- bottom: 1rem /* Position from the bottom */
2044
+ bottom: $ba /* Position from the bottom */
1538
2045
  left: 50% /* Center horizontally */
1539
2046
  transform: translateX(-50%) /* Adjust for centering */
1540
2047
  display: flex
1541
2048
  justify-content: center
1542
2049
  align-items: center
1543
2050
  gap: $xs
1544
- height: 2.5rem
2051
+ height: $lg
1545
2052
  background: $neutral-1
1546
2053
  border: 1px solid #e0e0e0
1547
2054
  border-radius: $sm /* Rounded corners */
@@ -1587,6 +2094,15 @@ svg
1587
2094
  :deep(.match-date)
1588
2095
  fill: #566176
1589
2096
 
2097
+ :deep(.match-venue)
2098
+ fill: #566176
2099
+
2100
+ :deep(.game-number)
2101
+ fill: #566176
2102
+
2103
+ :deep(.game-alias-label)
2104
+ fill: #566176
2105
+
1590
2106
  :deep(.edge-path)
1591
2107
  fill: none !important
1592
2108
  stroke: $neutral-4
@@ -1598,6 +2114,7 @@ svg
1598
2114
  opacity: 1
1599
2115
 
1600
2116
  :deep(.dot)
2117
+
1601
2118
  opacity: 0.6
1602
2119
 
1603
2120
  :deep(.remove-team .button-label)
@@ -1648,4 +2165,17 @@ svg
1648
2165
  pointer-events: auto
1649
2166
  box-shadow: 0 2px 8px rgba(0,0,0,0.15)
1650
2167
  overflow: hidden
2168
+
2169
+ :deep(.edge-alias-bg)
2170
+ shape-rendering: inherit
2171
+ stroke: $neutral-4 !important
2172
+ stroke-width: 2 !important
2173
+ fill: #fff !important
2174
+
2175
+ :deep(.venue-ellipsis)
2176
+ overflow: hidden
2177
+ text-overflow: ellipsis
2178
+ display: -webkit-box
2179
+ -webkit-line-clamp: 2
2180
+ -webkit-box-orient: vertical
1651
2181
  </style>