@usssa/component-library 1.0.0-alpha.263 → 1.0.0-alpha.264

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.263",
3
+ "version": "1.0.0-alpha.264",
4
4
  "description": "A Quasar component library project",
5
5
  "productName": "Quasar component library App",
6
6
  "author": "Engineering Team <engineering@usssa.com>",
@@ -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',
@@ -99,6 +104,7 @@ const tempSelectedTeamId = ref('')
99
104
  const teamSheet = ref([])
100
105
  const tooltipState = ref({ visible: false, x: 0, y: 0, text: '' })
101
106
  const zoomRef = ref(null)
107
+ const locationSheet = ref([])
102
108
 
103
109
  const selectedTeamName = computed({
104
110
  get() {
@@ -838,44 +844,131 @@ const renderBracket = () => {
838
844
  const destPos = getNodePosition(destId)
839
845
 
840
846
  if (pos1 && pos2 && destPos) {
841
- // Horizontally, it should be halfway between the end of the source node and the start of the destination node.
842
- const x = pos1.x + nodeWidth / 2
843
- // Vertically, it should be centered between the two source nodes.
844
- 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
845
858
 
846
- const dateGroup = g.append('g').attr('transform', `translate(${x}, ${y})`)
859
+ const detailsGroup = g.append('g').attr('transform', `translate(${startX}, ${centerY})`)
847
860
 
848
- // Add the date text first
849
- dateGroup
861
+ // Remove the white background rect (unwanted)
862
+ // detailsGroup.append('rect') ... (removed)
863
+
864
+ // Add the date text
865
+ detailsGroup
850
866
  .append('text')
851
867
  .attr('class', 'match-date text-overline-xs text-description')
852
- .attr('x', 30) // Position text to end just left of center
853
- .attr('y', 2)
854
- .attr('text-anchor', 'end') // Anchor text from the end
868
+ .attr('x', 0)
869
+ .attr('y', -6)
870
+ .attr('text-anchor', 'start')
855
871
  .attr('dominant-baseline', 'middle')
856
872
  .text(game.date)
857
873
 
858
- // Add the icon after the text
859
- 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
860
920
  .append('image')
861
921
  .attr('href', '/icons/field-location.svg')
862
- .attr('x', 95)
863
- .attr('y', -12)
922
+ .attr('x', 200)
923
+ .attr('y', -8)
864
924
  .attr('width', 24)
865
925
  .attr('height', 24)
866
926
  .attr('class', 'location-icon')
867
927
  .style('cursor', 'pointer')
868
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
+
869
939
  locationIcon.on('click', function (event) {
870
- const iconRect = this.getBoundingClientRect()
871
- const svgRect = svgRef.value.getBoundingClientRect()
872
- tooltipState.value = {
873
- visible: true,
874
- x: iconRect.left - svgRect.left + iconRect.width / 2,
875
- y: iconRect.top - svgRect.top - 40,
876
- 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
+ }
877
951
  }
878
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
+ })
879
972
  }
880
973
  })
881
974
 
@@ -903,6 +996,39 @@ const renderBracket = () => {
903
996
  (winnerNodePosition.x !== 0 || winnerNodePosition.y !== 0)
904
997
  ) {
905
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
+
906
1032
  game.team.forEach((source) => {
907
1033
  let sourceNodeId
908
1034
  if (
@@ -945,6 +1071,47 @@ const renderBracket = () => {
945
1071
  .attr('d', path.toString())
946
1072
  .attr('stroke-linecap', 'round')
947
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
+ }
948
1115
  }
949
1116
  }
950
1117
 
@@ -969,6 +1136,32 @@ const renderBracket = () => {
969
1136
  (loserNodePosition.x !== 0 || loserNodePosition.y !== 0)
970
1137
  ) {
971
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
+
972
1165
  game.team.forEach((source) => {
973
1166
  let sourceNodeId
974
1167
  if (
@@ -999,18 +1192,74 @@ const renderBracket = () => {
999
1192
  .attr('d', path.toString())
1000
1193
  .attr('stroke-linecap', 'round') // Changed to rounded end
1001
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
+ }
1002
1240
  }
1003
- }
1004
- })
1241
+ } // <-- FIX: close the LOSER edges block here
1242
+
1243
+ }) // <-- close games.value.forEach
1005
1244
 
1006
1245
  // After all edges are drawn, move highlighted edges to top
1007
1246
  nextTick(() => {
1008
1247
  const gEl = gRef.value
1009
1248
  if (gEl) {
1249
+ // Move highlighted edges to top
1010
1250
  const highlightedEdges = gEl.querySelectorAll('.edge-path.highlighted')
1011
1251
  highlightedEdges.forEach((el) => {
1012
1252
  gEl.appendChild(el)
1013
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
+ })
1014
1263
  }
1015
1264
  })
1016
1265
  }
@@ -1709,6 +1958,26 @@ watch(currentTransform, (newVal) => {
1709
1958
  />
1710
1959
  </template>
1711
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>
1712
1981
  </template>
1713
1982
 
1714
1983
  <style scoped lang="sass">
@@ -1825,6 +2094,15 @@ svg
1825
2094
  :deep(.match-date)
1826
2095
  fill: #566176
1827
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
+
1828
2106
  :deep(.edge-path)
1829
2107
  fill: none !important
1830
2108
  stroke: $neutral-4
@@ -1836,6 +2114,7 @@ svg
1836
2114
  opacity: 1
1837
2115
 
1838
2116
  :deep(.dot)
2117
+
1839
2118
  opacity: 0.6
1840
2119
 
1841
2120
  :deep(.remove-team .button-label)
@@ -1886,4 +2165,17 @@ svg
1886
2165
  pointer-events: auto
1887
2166
  box-shadow: 0 2px 8px rgba(0,0,0,0.15)
1888
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
1889
2181
  </style>
@@ -47,6 +47,8 @@
47
47
  }
48
48
  }
49
49
  ],
50
+ "date": "Sun. Feb 23, 2024 09:00 AM",
51
+ "venue": "Truckee High School 11725 Donner Pass Rd, Truckee, CA 96161",
50
52
  "position": {
51
53
  "columnNo": "1",
52
54
  "rowNo": "4"
@@ -123,6 +125,8 @@
123
125
  }
124
126
  }
125
127
  ],
128
+ "date": "Sun. Feb 23, 2024 09:00 AM",
129
+ "venue": "Truckee High School 11725 Donner Pass Rd, Truckee, CA 96161",
126
130
  "position": {
127
131
  "columnNo": "1",
128
132
  "rowNo": "8"
@@ -664,6 +668,8 @@
664
668
  }
665
669
  }
666
670
  ],
671
+ "date": "Sun. Feb 23, 2024 09:00 AM",
672
+ "venue": "Truckee High School 11725 Donner Pass Rd, Truckee, CA 96161",
667
673
  "position": {
668
674
  "columnNo": "2",
669
675
  "rowNo": "11"
@@ -901,6 +907,8 @@
901
907
  }
902
908
  }
903
909
  ],
910
+ "date": "Sun. Feb 23, 2024 09:00 AM",
911
+ "venue": "Truckee High School 11725 Donner Pass Rd, Truckee, CA 96161",
904
912
  "position": {
905
913
  "columnNo": "3",
906
914
  "rowNo": "9"