@usssa/component-library 1.0.0-alpha.243 → 1.0.0-alpha.245

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Component Library v1.0.0-alpha.243
1
+ # Component Library v1.0.0-alpha.244
2
2
 
3
3
  **This library provides custom UI components for USSSA applications**
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usssa/component-library",
3
- "version": "1.0.0-alpha.243",
3
+ "version": "1.0.0-alpha.245",
4
4
  "description": "A Quasar component library project",
5
5
  "productName": "Quasar component library App",
6
6
  "author": "Engineering Team <engineering@usssa.com>",
@@ -73,6 +73,7 @@ const bracketSides = [
73
73
  ]
74
74
  const nodeHeight = 40
75
75
  const nodeWidth = 240
76
+
76
77
  const self = 'bottom middle'
77
78
  const svgWidth = 1400
78
79
 
@@ -80,6 +81,12 @@ const activeRound = ref({ roundNo: 1, columnNo: 3 })
80
81
  const bracketSheet = ref([])
81
82
  const currentTransform = ref(d3.zoomIdentity)
82
83
  const gRef = ref(null)
84
+ const layoutCache = ref({
85
+ contentWidth: 0,
86
+ contentHeight: 0,
87
+ maxRow: 0,
88
+ numColumns: 0,
89
+ })
83
90
  const nodeGridPositions = ref({})
84
91
  const openMenu = ref(false)
85
92
  const openRoundMenu = ref(false)
@@ -225,13 +232,12 @@ const cancelTeamSheetSelection = () => {
225
232
  tempSelectedTeamId.value = selectedTeamId.value
226
233
  }
227
234
 
228
- // Compute all node positions using a row/column approach
235
+ // Compute all node positions and cache layout values
229
236
  const computeBracketLayout = () => {
230
237
  const newPositions = {}
231
238
 
232
239
  // Scan through all games to find every node with an explicit position
233
240
  games.value.forEach((game) => {
234
- // 1. Position the source teams for the game
235
241
  game.team.forEach((team) => {
236
242
  if (typeof team === 'object' && team.teamName && team.position) {
237
243
  const compositeId = `${team.teamName}_${team.position.columnNo}_${team.position.rowNo}`
@@ -241,8 +247,6 @@ const computeBracketLayout = () => {
241
247
  }
242
248
  }
243
249
  })
244
-
245
- // 2. Position the destination (winner) node for the game
246
250
  if (
247
251
  game.winnerDestination &&
248
252
  typeof game.winnerDestination === 'object' &&
@@ -261,8 +265,6 @@ const computeBracketLayout = () => {
261
265
  }
262
266
  }
263
267
  }
264
-
265
- // 3. Position the loser destination node for the game
266
268
  if (
267
269
  game.loserDestination &&
268
270
  typeof game.loserDestination === 'object' &&
@@ -282,8 +284,6 @@ const computeBracketLayout = () => {
282
284
  }
283
285
  }
284
286
  })
285
-
286
- // Add winnerPlace and loserPlace nodes from each round
287
287
  props.bracketData.round.forEach((round) => {
288
288
  if (Array.isArray(round.winnerPlace)) {
289
289
  round.winnerPlace.forEach((place) => {
@@ -308,8 +308,23 @@ const computeBracketLayout = () => {
308
308
  })
309
309
  }
310
310
  })
311
-
312
311
  nodeGridPositions.value = newPositions
312
+
313
+ // Cache layout values
314
+ const uniqueColumns = [
315
+ ...new Set(props.bracketData.round.map((r) => r.columnNo)),
316
+ ]
317
+ const numColumns = uniqueColumns.length
318
+ let maxRow = 0
319
+ Object.values(newPositions).forEach(({ row }) => {
320
+ if (row > maxRow) maxRow = row
321
+ })
322
+ layoutCache.value.numColumns = numColumns
323
+ layoutCache.value.maxRow = maxRow
324
+ layoutCache.value.contentWidth =
325
+ 50 + (numColumns - 1) * props.hGap + nodeWidth + 50
326
+ layoutCache.value.contentHeight =
327
+ 50 + (maxRow - 1) * props.vGap + nodeHeight + 50
313
328
  }
314
329
 
315
330
  const descriptiveRoundName = (round) => {
@@ -656,7 +671,7 @@ const renderBracket = () => {
656
671
  nodeGroups
657
672
  .append('text')
658
673
  .attr('class', 'node-label')
659
- .attr('x', (d) => d.x + 54) // Move label right only for seed teams
674
+ .attr('x', (d) => d.x + (seedTeamIds.includes(d.id) ? 50 : 40)) // Move label right only for seed teams
660
675
  .attr('y', (d) => d.y + nodeHeight / 2 + 5)
661
676
  .attr('text-anchor', (d) =>
662
677
  seedTeamIds.includes(d.id) ? 'start' : 'middle'
@@ -877,6 +892,38 @@ const renderBracket = () => {
877
892
  })
878
893
  }
879
894
 
895
+ function resetZoom() {
896
+ const svg = d3.select(svgRef.value)
897
+ if (zoomRef.value && svgRef.value) {
898
+ const svgElement = svgRef.value
899
+ const { width, height } = svgElement.getBoundingClientRect()
900
+ const centerX = width / 2
901
+ const centerY = height / 2
902
+
903
+ const k = 1
904
+ const contentWidth = layoutCache.value.contentWidth
905
+ const contentHeight = layoutCache.value.contentHeight
906
+ const viewportWidth = svgElement.clientWidth
907
+ const viewportHeight = svgElement.clientHeight
908
+ const minTx = viewportWidth - contentWidth * k
909
+ const maxTx = 0
910
+ const minTy = viewportHeight - contentHeight * k
911
+ const maxTy = 0
912
+ let tx = centerX
913
+ let ty = centerY
914
+ tx = minTx > 0 ? minTx / 2 : Math.max(minTx, Math.min(maxTx, tx))
915
+ ty = minTy > 0 ? minTy / 2 : Math.max(minTy, Math.min(maxTy, ty))
916
+ const clampedTransform = d3.zoomIdentity.translate(tx, ty).scale(k)
917
+ svg
918
+ .transition()
919
+ .duration(400)
920
+ .ease(d3.easeCubic)
921
+ .call(zoomRef.value.transform, clampedTransform)
922
+ d3.select(gRef.value).attr('transform', clampedTransform)
923
+ currentTransform.value = clampedTransform
924
+ }
925
+ }
926
+
880
927
  const scrollToBracketSide = (side) => {
881
928
  if (!props.bracketData || !props.bracketData.round) return
882
929
 
@@ -953,6 +1000,8 @@ const setSelectedTeamId = (teamId) => {
953
1000
  const team = toolbarTeams.value.find((t) => t.id === teamId)
954
1001
  if (team) {
955
1002
  selectedTeamId.value = team.id
1003
+ // Reset zoom to initial scale and center
1004
+ resetZoom()
956
1005
  }
957
1006
  openMenu.value = false
958
1007
  }
@@ -975,31 +1024,57 @@ const toggleRoundInSheet = (round) => {
975
1024
 
976
1025
  const zoomIn = () => {
977
1026
  const svg = d3.select(svgRef.value)
978
- if (zoomRef.value) {
1027
+ if (zoomRef.value && svgRef.value) {
1028
+ const svgElement = svgRef.value
1029
+ const { width, height } = svgElement.getBoundingClientRect()
1030
+ const centerX = width / 2
1031
+ const centerY = height / 2
1032
+
979
1033
  // 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)
1034
+ const prevScale = currentTransform.value.k || 1
1035
+ const newScale = Math.min(prevScale * 1.2, 2.5)
1036
+
1037
+ // Calculate new translation to keep center in view
1038
+ const newX =
1039
+ centerX - (centerX - currentTransform.value.x) * (newScale / prevScale)
1040
+ const newY =
1041
+ centerY - (centerY - currentTransform.value.y) * (newScale / prevScale)
1042
+
1043
+ const newTransform = d3.zoomIdentity.translate(newX, newY).scale(newScale)
1044
+
1045
+ svg
1046
+ .transition()
1047
+ .duration(200)
1048
+ .ease(d3.easeCubic)
1049
+ .call(zoomRef.value.transform, newTransform)
988
1050
  }
989
1051
  }
990
1052
 
991
1053
  const zoomOut = () => {
992
1054
  const svg = d3.select(svgRef.value)
993
- if (zoomRef.value) {
1055
+ if (zoomRef.value && svgRef.value) {
1056
+ const svgElement = svgRef.value
1057
+ const { width, height } = svgElement.getBoundingClientRect()
1058
+ const centerX = width / 2
1059
+ const centerY = height / 2
1060
+
994
1061
  // 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)
1062
+ const prevScale = currentTransform.value.k || 1
1063
+ const newScale = Math.max(prevScale / 1.2, 0.5)
1064
+
1065
+ // Calculate new translation to keep center in view
1066
+ const newX =
1067
+ centerX - (centerX - currentTransform.value.x) * (newScale / prevScale)
1068
+ const newY =
1069
+ centerY - (centerY - currentTransform.value.y) * (newScale / prevScale)
1070
+
1071
+ const newTransform = d3.zoomIdentity.translate(newX, newY).scale(newScale)
1072
+
1073
+ svg
1074
+ .transition()
1075
+ .duration(200)
1076
+ .ease(d3.easeCubic)
1077
+ .call(zoomRef.value.transform, newTransform)
1003
1078
  }
1004
1079
  }
1005
1080
 
@@ -1075,11 +1150,39 @@ onMounted(() => {
1075
1150
  const zoomBehavior = d3
1076
1151
  .zoom()
1077
1152
  .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.
1153
+ // Custom filter to intercept wheel events and always zoom centered
1080
1154
  .filter((event) => {
1081
1155
  if (event.type === 'wheel') {
1082
1156
  event.preventDefault()
1157
+ // Manually handle wheel zoom centered on SVG
1158
+ const svgElement = svgRef.value
1159
+ const { width, height } = svgElement.getBoundingClientRect()
1160
+ const centerX = width / 2
1161
+ const centerY = height / 2
1162
+ const prevScale = currentTransform.value.k || 1
1163
+ let newScale = prevScale
1164
+ // D3 uses event.deltaY for wheel direction
1165
+ if (event.deltaY < 0) {
1166
+ newScale = Math.min(prevScale * 1.2, 2.5)
1167
+ } else {
1168
+ newScale = Math.max(prevScale / 1.2, 0.5)
1169
+ }
1170
+ // Calculate new translation to keep center in view
1171
+ const newX =
1172
+ centerX -
1173
+ (centerX - currentTransform.value.x) * (newScale / prevScale)
1174
+ const newY =
1175
+ centerY -
1176
+ (centerY - currentTransform.value.y) * (newScale / prevScale)
1177
+ const newTransform = d3.zoomIdentity
1178
+ .translate(newX, newY)
1179
+ .scale(newScale)
1180
+ d3.select(svgRef.value)
1181
+ .transition()
1182
+ .duration(200)
1183
+ .ease(d3.easeCubic)
1184
+ .call(zoomBehavior.transform, newTransform)
1185
+ return false // Prevent default D3 wheel zoom
1083
1186
  }
1084
1187
  // Allow click-and-drag panning
1085
1188
  return !event.ctrlKey || event.type === 'wheel'
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
- import { ref } from 'vue'
2
+ import { nextTick, ref, watch } from 'vue'
3
3
  import URadioStd from './URadioStd.vue'
4
+ import UTooltip from './UTooltip.vue'
4
5
 
5
6
  const emit = defineEmits(['handleSelectChange'])
6
7
  const isSelected = defineModel('isSelected', {
@@ -71,6 +72,17 @@ const props = defineProps({
71
72
  })
72
73
 
73
74
  const isExpanded = ref(false)
75
+ const isOverflowing = ref(false)
76
+ const labelRef = ref(null)
77
+
78
+ const checkOverflow = () => {
79
+ nextTick(() => {
80
+ if (labelRef.value) {
81
+ const el = labelRef.value
82
+ isOverflowing.value = el.scrollWidth > el.clientWidth
83
+ }
84
+ })
85
+ }
74
86
 
75
87
  const handleChange = (value) => {
76
88
  emit('handleSelectChange', value)
@@ -89,6 +101,16 @@ const toggleExpansion = () => {
89
101
  isExpanded.value = !isExpanded.value
90
102
  }
91
103
  }
104
+
105
+ watch(
106
+ () => props.label,
107
+ () => {
108
+ checkOverflow()
109
+ },
110
+ {
111
+ immediate: true,
112
+ }
113
+ )
92
114
  </script>
93
115
 
94
116
  <template>
@@ -139,7 +161,20 @@ const toggleExpansion = () => {
139
161
  </q-icon>
140
162
  <slot name="avatar" />
141
163
  <div class="column u-expansion-label">
142
- <span class="text-caption-lg">{{ label }}</span>
164
+ <span
165
+ :class="`text-caption-lg text-truncate ${
166
+ isOverflowing ? `cursor-pointer` : ``
167
+ }`"
168
+ ref="labelRef"
169
+ @mouseenter="checkOverflow"
170
+ >{{ label }}</span
171
+ >
172
+ <UTooltip
173
+ v-if="isOverflowing"
174
+ anchor="bottom middle"
175
+ :description="label"
176
+ :offset="[14, 14]"
177
+ />
143
178
  <span class="text-body-xs">{{ caption }}</span>
144
179
  </div>
145
180
  <q-icon
@@ -209,7 +244,20 @@ const toggleExpansion = () => {
209
244
  </q-icon>
210
245
  <slot name="avatar" />
211
246
  <div class="column u-expansion-label">
212
- <span class="text-caption-lg">{{ label }}</span>
247
+ <span
248
+ :class="`text-caption-lg text-truncate ${
249
+ isOverflowing ? `cursor-pointer` : ``
250
+ }`"
251
+ ref="labelRef"
252
+ @mouseenter="checkOverflow"
253
+ >{{ label }}</span
254
+ >
255
+ <UTooltip
256
+ v-if="isOverflowing"
257
+ anchor="bottom middle"
258
+ :description="label"
259
+ :offset="[14, 14]"
260
+ />
213
261
  <span class="text-body-xs text-description">{{ caption }}</span>
214
262
  </div>
215
263
  <q-icon
@@ -254,6 +302,13 @@ const toggleExpansion = () => {
254
302
  word-break: break-word
255
303
  overflow-wrap: break-word
256
304
 
305
+ .text-truncate
306
+ overflow: hidden
307
+ text-overflow: ellipsis
308
+ white-space: nowrap
309
+ display: block
310
+ max-width: 100%
311
+
257
312
  .q-expansion-item__content > :first-child
258
313
  padding: $xs $ba $ba $ba
259
314
 
@@ -1671,6 +1671,11 @@ window.addEventListener('resize', handleCellOverflowResize)
1671
1671
  :color="props?.row?.iconColor ?? 'primary'"
1672
1672
  size="1.5rem"
1673
1673
  />
1674
+ <div v-if="typeof col?.colorMap === 'function'" class="flex items-baseline" :style="{ gap: '0.25rem',}">
1675
+ <span :class="`table-text-colormap-${col?.colorMap(props.row)?.color} text-caption-md`"> {{ col?.colorMap(props.row)?.value }}</span>
1676
+ <span class="text-body-sm">{{ props.row[col.field] }}</span>
1677
+ </div>
1678
+
1674
1679
  <!-- Text -->
1675
1680
  <span v-else class="text-body-sm">{{ props.row[col.field] }}</span>
1676
1681
  </div>
@@ -738,8 +738,11 @@ watch(
738
738
  col.combineColoumn(props.row)
739
739
  ? ' combine-col-wrapper'
740
740
  : ''
741
- }`"
741
+ } ${typeof col?.colorMap === 'function' ? `combine-col-wrapper`: ``}`"
742
742
  >
743
+ <span v-if="typeof col?.colorMap === 'function'" :class="`table-text-colormap-${col?.colorMap(props.row)?.color} text-caption-md`">
744
+ {{ col?.colorMap(props.row)?.value }}
745
+ </span>
743
746
  <div v-if="col.type !== 'icon'">
744
747
  {{ props.row[col.field] }}
745
748
  </div>
@@ -1691,4 +1694,7 @@ watch(
1691
1694
  white-space: nowrap
1692
1695
  display: block
1693
1696
 
1697
+ .table-text-colormap-positive
1698
+ color: $green-6 !important
1699
+
1694
1700
  </style>