@usssa/component-library 1.0.0-alpha.253 → 1.0.0-alpha.254

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usssa/component-library",
3
- "version": "1.0.0-alpha.253",
3
+ "version": "1.0.0-alpha.254",
4
4
  "description": "A Quasar component library project",
5
5
  "productName": "Quasar component library App",
6
6
  "author": "Engineering Team <engineering@usssa.com>",
@@ -116,67 +116,62 @@ const isTeamSelected = computed(() => selectedTeamName.value.length > 0)
116
116
 
117
117
  // Flatten for rendering logic
118
118
  const games = computed(() =>
119
- props.bracketData.round.flatMap((r, i) =>
119
+ normalizedBracketData.value.round.flatMap((r, i) =>
120
120
  r.game.map((m) => ({ ...m, round: r.roundNo }))
121
121
  )
122
122
  )
123
123
 
124
+ // Use normalized bracket data for all rendering and layout logic
125
+ const normalizedBracketData = computed(() =>
126
+ normalizeBracketData(props.bracketData)
127
+ )
128
+
124
129
  const roundLabelStyles = computed(() => {
125
130
  const t = currentTransform.value || d3.zoomIdentity
126
-
127
- // 1. Group rounds by column number to handle multiple rounds in the same visual column
128
- const roundsByColumn = props.bracketData.round.reduce((acc, round) => {
129
- const col = round.columnNo
130
- if (!acc[col]) {
131
- acc[col] = []
132
- }
133
- acc[col].push(round)
134
- return acc
135
- }, {})
136
-
137
- // 2. Get sorted column numbers
138
- const sortedColumns = Object.keys(roundsByColumn).sort((a, b) => a - b)
139
-
131
+ // Group rounds by normalized column number
132
+ const roundsByColumn = normalizedBracketData.value.round.reduce(
133
+ (acc, round) => {
134
+ const col = Number(round.columnNo)
135
+ if (!acc[col]) {
136
+ acc[col] = []
137
+ }
138
+ acc[col].push(round)
139
+ return acc
140
+ },
141
+ {}
142
+ )
143
+ // Get sorted normalized column numbers
144
+ const sortedColumns = Object.keys(roundsByColumn)
145
+ .map(Number)
146
+ .sort((a, b) => a - b)
140
147
  const styles = []
141
-
142
- // 3. Generate styles for each round
143
- props.bracketData.round.forEach((round) => {
144
- const col = round.columnNo
145
- const columnIndex = sortedColumns.indexOf(col.toString())
148
+ // Generate styles for each round using normalized column
149
+ normalizedBracketData.value.round.forEach((round, idx) => {
150
+ const col = Number(round.columnNo)
151
+ const columnIndex = sortedColumns.indexOf(col)
146
152
  const isLastColumn = columnIndex === sortedColumns.length - 1
147
-
148
153
  // The base X position is the same for all rounds in the same column
149
154
  const baseX = (col - 1) * props.hGap
150
-
151
- // The width of the label container should be the horizontal gap
152
155
  const baseWidth = props.hGap
153
-
154
156
  // Apply zoom and pan transformations
155
157
  const left = t.applyX(baseX)
156
158
  const width = baseWidth * t.k
157
-
158
159
  const style = {
159
160
  left: left + 'px',
160
161
  width: width + 'px',
161
162
  }
162
-
163
- // Add a border to all but the last column
164
163
  if (!isLastColumn) {
165
164
  style.borderRight = '1px solid #B9E0FF'
166
165
  }
167
-
168
- // Find the original index to push the style in the correct order for the template v-for
169
- const originalIndex = props.bracketData.round.findIndex((r) => r === round)
170
- styles[originalIndex] = style
166
+ styles[idx] = style
171
167
  })
172
-
173
168
  return styles
174
169
  })
175
170
 
176
- // Compute teams for toolbar selection from bracket data (selectionType: 'Seed')
171
+ // Compute teams for toolbar selection from normalized bracket data (selectionType: 'Seed')
177
172
  const toolbarTeams = computed(() => {
178
173
  const seedTeams = []
179
- props.bracketData.round.forEach((round) => {
174
+ normalizedBracketData.value.round.forEach((round) => {
180
175
  if (Array.isArray(round.game)) {
181
176
  round.game.forEach((game) => {
182
177
  if (Array.isArray(game.team)) {
@@ -207,7 +202,7 @@ const applyBracketSheetSelection = () => {
207
202
 
208
203
  const applyRoundSheetSelection = () => {
209
204
  const selectedRound = props.bracketData.round.find(
210
- (r) => `${r.roundNo}-${r.columnNo}` === tempSelectedRoundId.value
205
+ (r) => `${r.roundNo}-${r.roundName}` === tempSelectedRoundId.value
211
206
  )
212
207
  if (selectedRound) scrollToRound(selectedRound)
213
208
  roundSheet.value = []
@@ -458,6 +453,136 @@ const getTeamPath = (teamId) => {
458
453
  }
459
454
  }
460
455
 
456
+ const normalizeBracketData = (bracketData) => {
457
+ const minCol = bracketData.minColumn
458
+ ? Number(bracketData.minColumn)
459
+ : Math.min(...bracketData.round.map((r) => Number(r.columnNo)))
460
+ const minRow = bracketData.minRow
461
+ ? Number(bracketData.minRow)
462
+ : Math.min(
463
+ ...bracketData.round
464
+ .flatMap((r) =>
465
+ (r.game || []).flatMap((g) =>
466
+ (g.team || []).map((t) => Number(t.position?.rowNo) || 0)
467
+ )
468
+ )
469
+ .filter((n) => n > 0)
470
+ )
471
+
472
+ // Normalize all positions so columns/rows start from 1
473
+ const normRounds = []
474
+ for (const round of bracketData.round) {
475
+ const normGames = []
476
+ if (Array.isArray(round.game)) {
477
+ for (const game of round.game) {
478
+ const normTeams = []
479
+ if (Array.isArray(game.team)) {
480
+ for (const team of game.team) {
481
+ if (team.position) {
482
+ normTeams.push({
483
+ ...team,
484
+ position: {
485
+ columnNo: Number(team.position.columnNo) - minCol + 1,
486
+ rowNo: Number(team.position.rowNo) - minRow + 1,
487
+ },
488
+ })
489
+ } else {
490
+ normTeams.push(team)
491
+ }
492
+ }
493
+ }
494
+ let normWinnerDest = game.winnerDestination
495
+ if (
496
+ normWinnerDest &&
497
+ normWinnerDest.team &&
498
+ normWinnerDest.team.position
499
+ ) {
500
+ normWinnerDest = {
501
+ ...normWinnerDest,
502
+ team: {
503
+ ...normWinnerDest.team,
504
+ position: {
505
+ columnNo:
506
+ Number(normWinnerDest.team.position.columnNo) - minCol + 1,
507
+ rowNo: Number(normWinnerDest.team.position.rowNo) - minRow + 1,
508
+ },
509
+ },
510
+ }
511
+ }
512
+ let normLoserDest = game.loserDestination
513
+ if (
514
+ normLoserDest &&
515
+ normLoserDest.team &&
516
+ normLoserDest.team.position
517
+ ) {
518
+ normLoserDest = {
519
+ ...normLoserDest,
520
+ team: {
521
+ ...normLoserDest.team,
522
+ position: {
523
+ columnNo:
524
+ Number(normLoserDest.team.position.columnNo) - minCol + 1,
525
+ rowNo: Number(normLoserDest.team.position.rowNo) - minRow + 1,
526
+ },
527
+ },
528
+ }
529
+ }
530
+ normGames.push({
531
+ ...game,
532
+ team: normTeams,
533
+ winnerDestination: normWinnerDest,
534
+ loserDestination: normLoserDest,
535
+ })
536
+ }
537
+ }
538
+ // Normalize winnerPlace/loserPlace
539
+ const normWinnerPlace = []
540
+ if (Array.isArray(round.winnerPlace)) {
541
+ for (const place of round.winnerPlace) {
542
+ if (place.position) {
543
+ normWinnerPlace.push({
544
+ ...place,
545
+ position: {
546
+ columnNo: Number(place.position.columnNo) - minCol + 1,
547
+ rowNo: Number(place.position.rowNo) - minRow + 1,
548
+ },
549
+ })
550
+ } else {
551
+ normWinnerPlace.push(place)
552
+ }
553
+ }
554
+ }
555
+ const normLoserPlace = []
556
+ if (Array.isArray(round.loserPlace)) {
557
+ for (const place of round.loserPlace) {
558
+ if (place.position) {
559
+ normLoserPlace.push({
560
+ ...place,
561
+ position: {
562
+ columnNo: Number(place.position.columnNo) - minCol + 1,
563
+ rowNo: Number(place.position.rowNo) - minRow + 1,
564
+ },
565
+ })
566
+ } else {
567
+ normLoserPlace.push(place)
568
+ }
569
+ }
570
+ }
571
+ normRounds.push({
572
+ ...round,
573
+ columnNo: Number(round.columnNo) - minCol + 1,
574
+ roundName: round.roundName,
575
+ game: normGames,
576
+ winnerPlace: normWinnerPlace,
577
+ loserPlace: normLoserPlace,
578
+ })
579
+ }
580
+ return {
581
+ ...bracketData,
582
+ round: normRounds,
583
+ }
584
+ }
585
+
461
586
  const openExpandView = () => {
462
587
  emit('open-expand')
463
588
  }
@@ -672,11 +797,8 @@ const renderBracket = () => {
672
797
  nodeGroups
673
798
  .append('text')
674
799
  .attr('class', 'node-label')
675
- .attr('x', (d) => d.x + (seedTeamIds.includes(d.id) ? 50 : 40)) // Move label right only for seed teams
800
+ .attr('x', (d) => d.x + (seedTeamIds.includes(d.id) ? 50 : 10)) // Move label right only for seed teams
676
801
  .attr('y', (d) => d.y + nodeHeight / 2 + 5)
677
- .attr('text-anchor', (d) =>
678
- seedTeamIds.includes(d.id) ? 'start' : 'middle'
679
- )
680
802
  .text((d) => d.label)
681
803
 
682
804
  // Add '-' text to the right side for seed teams
@@ -912,7 +1034,8 @@ function resetZoom() {
912
1034
 
913
1035
  // Center on selected team if available, else round 1
914
1036
  let tx, ty
915
- let targetX = null, targetY = null
1037
+ let targetX = null,
1038
+ targetY = null
916
1039
  if (selectedTeamId.value) {
917
1040
  const pos = getNodePosition(selectedTeamId.value)
918
1041
  targetX = pos.x + nodeWidth / 2
@@ -943,22 +1066,18 @@ function resetZoom() {
943
1066
  }
944
1067
 
945
1068
  const scrollToBracketSide = (side) => {
946
- if (!props.bracketData || !props.bracketData.round) return
947
-
948
- const firstRound = props.bracketData.round[0]
1069
+ if (!normalizedBracketData.value || !normalizedBracketData.value.round) return
1070
+ const firstRound = normalizedBracketData.value.round[0]
949
1071
  if (!firstRound || !firstRound.game || firstRound.game.length === 0) return
950
-
951
1072
  const firstMatch = firstRound.game[0]
952
1073
  let targetColumn = null
953
-
954
1074
  if (side === 'winner' && firstMatch.winnerDestination) {
955
1075
  targetColumn = firstMatch.winnerDestination.team.position.columnNo
956
1076
  } else if (side === 'loser' && firstMatch.loserDestination) {
957
1077
  targetColumn = firstMatch.loserDestination.team.position.columnNo
958
1078
  }
959
-
960
1079
  if (targetColumn) {
961
- const targetRound = props.bracketData.round.find(
1080
+ const targetRound = normalizedBracketData.value.round.find(
962
1081
  (r) => r.columnNo === targetColumn
963
1082
  )
964
1083
  if (targetRound) {
@@ -969,41 +1088,26 @@ const scrollToBracketSide = (side) => {
969
1088
 
970
1089
  const scrollToRound = (round) => {
971
1090
  activeRound.value = { roundNo: round.roundNo, columnNo: round.columnNo }
972
- openRoundMenu.value = false // Close the menu on selection
1091
+ openRoundMenu.value = false
973
1092
  const svg = d3.select(svgRef.value)
974
1093
  const zoomBehavior = zoomRef.value
975
1094
  if (!zoomBehavior || !svgRef.value) return
976
1095
 
977
- // Calculate the x-coordinate of the target round's center.
978
- const roundX = (round.columnNo - 1) * props.hGap + props.hGap / 2
1096
+ const normalizedRound = normalizedBracketData.value.round
979
1097
 
980
- // Get the current viewport width.
981
- const viewportWidth = svgRef.value.clientWidth
1098
+ const mainRound = normalizedRound.find(
1099
+ (r) => r.roundName == round.roundName && r.roundNo == round.roundNo
1100
+ )
982
1101
 
983
- // Get the LIVE transform directly from the SVG node for accuracy.
984
- // This is the key fix: currentTransform.value can be stale during transitions.
1102
+ const roundX = (mainRound.columnNo - 1) * props.hGap + props.hGap / 2
1103
+ const viewportWidth = svgRef.value.clientWidth
985
1104
  const liveTransform = d3.zoomTransform(svg.node())
986
1105
  const { k: currentK, y: currentY } = liveTransform
987
-
988
- // Determine content dimensions.
989
- const uniqueColumns = [
990
- ...new Set(props.bracketData.round.map((r) => r.columnNo)),
991
- ]
992
- const numColumns = uniqueColumns.length
993
- const contentWidth = 50 + (numColumns - 1) * props.hGap + nodeWidth + 50
994
-
995
- // Calculate the new x-translate to center the round's center in the viewport.
996
1106
  const targetX = viewportWidth / 2 - roundX * currentK
997
-
998
1107
  const clampedX = targetX
999
-
1000
- // Create the new transform, keeping the current scale and y-pan.
1001
-
1002
1108
  const newTransform = d3.zoomIdentity
1003
1109
  .translate(clampedX, currentY)
1004
1110
  .scale(currentK)
1005
-
1006
- // Apply the new transform with a smooth transition.
1007
1111
  svg.transition().duration(750).call(zoomBehavior.transform, newTransform)
1008
1112
  }
1009
1113
 
@@ -1029,7 +1133,7 @@ const toggleBracketSideInSheet = (side) => {
1029
1133
  }
1030
1134
 
1031
1135
  const toggleRoundInSheet = (round) => {
1032
- tempSelectedRoundId.value = `${round.roundNo}-${round.columnNo}`
1136
+ tempSelectedRoundId.value = `${round.roundNo}-${round.roundName}`
1033
1137
  }
1034
1138
 
1035
1139
  const toggleTeamInSheet = (teamId) => {
@@ -1154,7 +1258,7 @@ onMounted(() => {
1154
1258
 
1155
1259
  // Determine content dimensions
1156
1260
  const uniqueColumns = [
1157
- ...new Set(props.bracketData.round.map((r) => r.columnNo)),
1261
+ ...new Set(normalizedBracketData.value.round.map((r) => r.columnNo)),
1158
1262
  ]
1159
1263
  const numColumns = uniqueColumns.length
1160
1264
  const contentWidth = 50 + (numColumns + 2) * props.hGap + nodeWidth + 50
@@ -1203,29 +1307,21 @@ onMounted(() => {
1203
1307
  .call(zoomBehavior.transform, newTransform)
1204
1308
  return false // Prevent default D3 wheel zoom
1205
1309
  }
1206
- // Allow click-and-drag panning
1207
1310
  return !event.ctrlKey || event.type === 'wheel'
1208
1311
  })
1209
- // We clamp manually in the zoom event instead of using translateExtent
1210
1312
  .on('zoom', (event) => {
1211
1313
  let { transform } = event
1212
1314
  const { k } = transform
1213
- tooltipState.value.visible = false // Hide tooltip on zoom/pan
1214
-
1215
- // Calculate boundaries for clamping.
1315
+ tooltipState.value.visible = false
1216
1316
  const minTx = viewportWidth - contentWidth * k
1217
1317
  const maxTx = 0
1218
1318
  const minTy = viewportHeight - contentHeight * k
1219
1319
  const maxTy = 0
1220
-
1221
1320
  const tx =
1222
1321
  minTx > 0 ? minTx / 2 : Math.max(minTx, Math.min(maxTx, transform.x))
1223
1322
  const ty =
1224
1323
  minTy > 0 ? minTy / 2 : Math.max(minTy, Math.min(maxTy, transform.y))
1225
-
1226
- // Create a new clamped transform
1227
1324
  const clampedTransform = d3.zoomIdentity.translate(tx, ty).scale(k)
1228
-
1229
1325
  g.attr('transform', clampedTransform)
1230
1326
  currentTransform.value = clampedTransform
1231
1327
  })
@@ -1233,19 +1329,40 @@ onMounted(() => {
1233
1329
  svg.call(zoomBehavior)
1234
1330
  zoomRef.value = zoomBehavior
1235
1331
 
1236
- // Focus on round 1 when bracket loads
1237
- const firstRound = props.bracketData.round?.[0]
1332
+ // Focus on first normalized round when bracket loads
1333
+ const firstRound = normalizedBracketData.value.round?.[0]
1238
1334
  if (firstRound) scrollToRound(firstRound)
1239
1335
 
1240
1336
  renderBracket()
1241
1337
 
1242
- // Add window resize listener
1243
1338
  window.addEventListener('resize', reinitZoomAndLayout)
1244
1339
  })
1245
1340
 
1246
1341
  onUnmounted(() => {
1247
1342
  window.removeEventListener('resize', reinitZoomAndLayout)
1248
- document.removeEventListener('click', () => {})
1343
+ })
1344
+
1345
+ watch(
1346
+ () => props.bracketData,
1347
+ () => {
1348
+ // Reset selections on bracket data change
1349
+ selectedTeamId.value = ''
1350
+ tempSelectedTeamId.value = ''
1351
+ tempSelectedRoundId.value = ''
1352
+ tempSelectedBracketSide.value = ''
1353
+ bracketSheet.value = []
1354
+ roundSheet.value = []
1355
+ computeBracketLayout()
1356
+ renderBracket()
1357
+ },
1358
+ { deep: true }
1359
+ )
1360
+
1361
+ watch(currentTransform, (newVal) => {
1362
+ // Only update on explicit user zoom/pan, not on initial mount
1363
+ if (gRef.value) {
1364
+ d3.select(gRef.value).attr('transform', newVal)
1365
+ }
1249
1366
  })
1250
1367
  </script>
1251
1368
 
@@ -1399,7 +1516,7 @@ onUnmounted(() => {
1399
1516
  @click="openRoundSheet"
1400
1517
  />
1401
1518
  <UBtnIcon
1402
- v-if="!$screen.isMobile"
1519
+ v-if="!$screen.isMobile && toolbarTeams.length"
1403
1520
  iconClass="fa-kit-duotone fa-jersey"
1404
1521
  :anchor="anchor"
1405
1522
  ariaLabel="Select Team"
@@ -1428,7 +1545,7 @@ onUnmounted(() => {
1428
1545
  </template>
1429
1546
  </UBtnIcon>
1430
1547
  <UBtnIcon
1431
- v-if="$screen.isMobile"
1548
+ v-if="$screen.isMobile && toolbarTeams.length"
1432
1549
  iconClass="fa-kit-duotone fa-jersey"
1433
1550
  :anchor="anchor"
1434
1551
  ariaLabel="Select Team"
@@ -1516,14 +1633,14 @@ onUnmounted(() => {
1516
1633
  in-sheet
1517
1634
  :label="descriptiveRoundName(round)"
1518
1635
  :selected="
1519
- tempSelectedRoundId === `${round.roundNo}-${round.columnNo}`
1636
+ tempSelectedRoundId === `${round.roundNo}-${round.roundName}`
1520
1637
  "
1521
1638
  @onClick="toggleRoundInSheet(round)"
1522
1639
  >
1523
1640
  <template #trailing_slot>
1524
1641
  <q-icon
1525
1642
  v-if="
1526
- tempSelectedRoundId === `${round.roundNo}-${round.columnNo}`
1643
+ tempSelectedRoundId === `${round.roundNo}-${round.roundName}`
1527
1644
  "
1528
1645
  class="fa-kit-duotone fa-circle-check"
1529
1646
  color="primary"
@@ -1652,7 +1769,7 @@ svg
1652
1769
  .main-content-dialog
1653
1770
  padding-top: $ba !important
1654
1771
  .q-item
1655
- margin-bottom: $xs
1772
+ margin-bottom: $xs !important
1656
1773
 
1657
1774
  .bracket-toolbar
1658
1775
  bottom: $ba /* Position from the bottom */