@dcrackel/hematournamentui 1.0.712 → 1.0.714

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.
Files changed (107) hide show
  1. package/.prettierignore +7 -7
  2. package/.storybook/main.js +3 -1
  3. package/README.md +42 -0
  4. package/dist/HemaTournamentUI-lib.es.js +6856 -6243
  5. package/dist/HemaTournamentUI-lib.umd.js +27 -27
  6. package/dist/hematournamentui-tailwind.css +79 -9
  7. package/package.json +96 -96
  8. package/prettier.config.js +16 -16
  9. package/src/index.js +43 -42
  10. package/src/main.js +1 -1
  11. package/src/mocks/getDEBouts-TableOf128.js +37 -37
  12. package/src/mocks/getDEBouts-TableOf64.js +35 -35
  13. package/src/stories/Atoms/Input/BaseInput.test.js +97 -97
  14. package/src/stories/Atoms/Input/BaseInput.vue +149 -149
  15. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.stories.js +23 -23
  16. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.vue +35 -35
  17. package/src/stories/Molecules/Indicators/RegistrationStatus/RegistrationStatus.vue +30 -30
  18. package/src/stories/Molecules/Menus/HoverActions/HoverActions.test.js +138 -0
  19. package/src/stories/Molecules/Menus/HoverActions/HoverActions.vue +212 -0
  20. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.stories.js +49 -0
  21. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.test.js +129 -0
  22. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.vue +165 -0
  23. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.stories.js +6 -0
  24. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.test.js +18 -18
  25. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.vue +1 -1
  26. package/src/stories/Organisms/Cards/BoutCard/BoutCard.vue +5 -1
  27. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardActive/BoutCardActive.vue +19 -10
  28. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardInactive/BoutCardInactive.vue +11 -7
  29. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +42 -33
  30. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +76 -45
  31. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +122 -103
  32. package/src/stories/Organisms/Cards/Director/DirectorCard.test.js +82 -0
  33. package/src/stories/Organisms/Cards/Director/DirectorCard.vue +32 -51
  34. package/src/stories/Organisms/Cards/EditBout/EditBout.test.js +51 -51
  35. package/src/stories/Organisms/Cards/EventCard/EventCard.test.js +106 -106
  36. package/src/stories/Organisms/Cards/FencerCard/FencerCard.vue +298 -298
  37. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.stories.js +77 -60
  38. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.test.js +144 -143
  39. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.vue +289 -256
  40. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.stories.js +69 -69
  41. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.vue +80 -80
  42. package/src/stories/Organisms/Cards/WaitlistFencerCard/WaitlistFencerCard.vue +153 -153
  43. package/src/stories/Organisms/ComplexInputs/DropDown/DropDownMenu.vue +4 -0
  44. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.stories.js +31 -31
  45. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.test.js +109 -109
  46. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue +192 -192
  47. package/src/stories/Organisms/Containers/Tableau/Tableau.stories.js +1 -0
  48. package/src/stories/Organisms/Containers/Tableau/Tableau.test.js +247 -204
  49. package/src/stories/Organisms/Containers/Tableau/Tableau.vue +89 -15
  50. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +51 -29
  51. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +63 -46
  52. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.test.js +21 -21
  53. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.vue +39 -39
  54. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.js +145 -145
  55. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.test.js +64 -64
  56. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.js +36 -57
  57. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.test.js +101 -65
  58. package/src/stories/Organisms/Containers/TableauColumn/TableauColumn.vue +14 -1
  59. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.stories.js +52 -52
  60. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.vue +10 -3
  61. package/src/stories/Organisms/Wizards/EditEventWizard/DirectElimination/DirectElimination.vue +226 -226
  62. package/src/stories/Organisms/Wizards/EditEventWizard/DirectEliminationPromotion/DirectEliminationPromotion.vue +204 -204
  63. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.stories.js +137 -137
  64. package/src/stories/Organisms/Wizards/EditEventWizard/EventCustomization/EventCustomization.vue +172 -172
  65. package/src/stories/Organisms/Wizards/EditEventWizard/PoolConfiguration/PoolConfiguration.vue +216 -216
  66. package/src/stories/Organisms/Wizards/EditEventWizard/PriceFields.test.js +79 -79
  67. package/src/stories/Organisms/Wizards/EditEventWizard/RuleSerialization.test.js +29 -29
  68. package/src/stories/Organisms/Wizards/EditEventWizard/WeaponSelection.test.js +138 -0
  69. package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +149 -9
  70. package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +54 -2
  71. package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +77 -8
  72. package/src/stories/Templates/EventManagement/Bracket/BracketDirectorAssignment.test.js +242 -0
  73. package/src/stories/Templates/EventManagement/Bracket/BracketMobile.test.js +131 -0
  74. package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +50 -39
  75. package/src/stories/Templates/EventManagement/Bracket/BracketStories.test.js +201 -0
  76. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.js +93 -0
  77. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.test.js +152 -0
  78. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.js +39 -39
  79. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.test.js +79 -79
  80. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.stories.js +93 -93
  81. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.vue +91 -91
  82. package/src/stories/Templates/EventManagement/PoolLive/PoolLive.test.js +121 -121
  83. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.stories.js +9 -2
  84. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.vue +5 -4
  85. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.stories.js +81 -81
  86. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.test.js +39 -39
  87. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.vue +101 -101
  88. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.stories.js +71 -71
  89. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.vue +86 -86
  90. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.stories.js +86 -86
  91. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.vue +247 -247
  92. package/src/stories/Templates/PersonManagement/Basic/BasicEdit.vue +170 -170
  93. package/src/stories/Templates/PersonManagement/PersonManagement.stories.js +156 -156
  94. package/src/stories/Templates/PersonManagement/PersonManagement.test.js +329 -329
  95. package/src/stories/Templates/PersonManagement/PersonManagement.vue +329 -329
  96. package/src/stories/Templates/PersonManagement/PersonManagementDraft.js +42 -42
  97. package/src/stories/Templates/TournamentManagement/TournamentManagement.stories.js +84 -84
  98. package/src/stories/Templates/TournamentManagement/TournamentManagement.test.js +114 -114
  99. package/src/stories/Templates/TournamentManagement/TournamentManagement.vue +363 -363
  100. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.stories.js +116 -116
  101. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.vue +307 -307
  102. package/src/stories/Util/Validations/PersonManagementValidations.js +45 -45
  103. package/src/stories/Util/Validations/PriceValidation.js +6 -6
  104. package/src/stories/Util/boutDirectorAssignment.js +46 -0
  105. package/src/stories/Util/bracketDirectors.js +86 -0
  106. package/src/stories/Util/registrationStatus.js +11 -11
  107. package/src/stories/Util/tabs.js +19 -47
@@ -0,0 +1,165 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <dialog
4
+ ref="dialog"
5
+ aria-label="DE directors"
6
+ data-testid="bracket-directors-modal"
7
+ class="fixed inset-0 m-auto max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-xl overflow-y-auto rounded-xl border border-dropdownSelect bg-neutral p-0 text-primaryHighlight shadow-xl backdrop:bg-secondary/60"
8
+ @cancel.prevent="close"
9
+ @click="close"
10
+ >
11
+ <form class="p-5" @submit.prevent="submit" @click.stop>
12
+ <div class="mb-4 flex items-start justify-between gap-3">
13
+ <div>
14
+ <h2 class="text-xl font-bold">Directors</h2>
15
+ <p class="mt-1 text-sm text-quaternary">Select DE directors and choose their rings.</p>
16
+ </div>
17
+ <button
18
+ type="button"
19
+ aria-label="Close director assignments"
20
+ class="flex h-8 w-8 flex-none items-center justify-center rounded text-xl text-quaternary hover:bg-dropdownSelect focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright"
21
+ @click="close"
22
+ >
23
+ &times;
24
+ </button>
25
+ </div>
26
+ <p v-if="!draft.length" class="rounded border border-dropdownSelect p-4 text-sm text-quaternary">
27
+ No directors available.
28
+ </p>
29
+ <div class="space-y-3">
30
+ <div
31
+ v-for="row in draft"
32
+ :key="row.Director.PersonId"
33
+ class="flex items-center gap-3 rounded-lg border border-dropdownSelect px-3 py-3"
34
+ >
35
+ <label class="flex min-w-0 flex-1 cursor-pointer items-center gap-3">
36
+ <input
37
+ v-model="row.Selected"
38
+ type="checkbox"
39
+ :data-director-id="row.Director.PersonId"
40
+ :aria-label="'Include ' + row.Director.DisplayName + ' in DE'"
41
+ class="h-5 w-5 flex-none accent-bright"
42
+ />
43
+ <span
44
+ v-if="row.Color"
45
+ class="h-2.5 w-2.5 flex-none rounded-full"
46
+ :style="{ backgroundColor: row.Color }"
47
+ aria-hidden="true"
48
+ ></span>
49
+ <span class="min-w-0 wrap-break-word text-sm font-semibold">{{ row.Director.DisplayName }}</span>
50
+ </label>
51
+ <select
52
+ v-model="row.RingId"
53
+ :disabled="!row.Selected"
54
+ :data-ring-director-id="row.Director.PersonId"
55
+ :aria-label="'Ring for ' + row.Director.DisplayName"
56
+ class="w-28 flex-none rounded border border-dropdownSelect bg-white px-2 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright disabled:opacity-50"
57
+ >
58
+ <option value="">Choose ring</option>
59
+ <option v-for="ring in ringOptions" :key="ring.RingId" :value="ring.RingId" :disabled="ring.Disabled">
60
+ {{ ring.RingName }}
61
+ </option>
62
+ </select>
63
+ </div>
64
+ </div>
65
+ <p v-if="error" role="alert" class="mt-3 text-sm text-alarm">{{ error }}</p>
66
+ <div class="mt-5 flex justify-end gap-2">
67
+ <button
68
+ type="button"
69
+ class="rounded border border-dropdownSelect px-4 py-2 text-sm hover:bg-dropdownSelect focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright"
70
+ @click="close"
71
+ >
72
+ Cancel
73
+ </button>
74
+ <button
75
+ type="submit"
76
+ :disabled="Boolean(error)"
77
+ class="rounded bg-primary px-4 py-2 text-sm font-semibold text-neutral hover:bg-primaryHighlight focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright disabled:cursor-not-allowed disabled:opacity-50"
78
+ >
79
+ Save assignments
80
+ </button>
81
+ </div>
82
+ </form>
83
+ </dialog>
84
+ </Teleport>
85
+ </template>
86
+
87
+ <script>
88
+ import {
89
+ createDirectorDraft,
90
+ reconcileDirectorDraft,
91
+ createDirectorStations,
92
+ getDirectorDraftError,
93
+ getRingOptions
94
+ } from '../../../Util/bracketDirectors.js';
95
+
96
+ export default {
97
+ name: 'BracketDirectorsModal',
98
+ emits: ['update:show', 'submit:stations'],
99
+ props: {
100
+ show: { type: Boolean, default: false },
101
+ availableDirectors: { type: Array, default: () => [] },
102
+ rings: { type: Array, default: () => [] },
103
+ stations: { type: Array, default: () => [] }
104
+ },
105
+ data() {
106
+ return { draft: [] };
107
+ },
108
+ computed: {
109
+ ringOptions() {
110
+ return getRingOptions(this.rings);
111
+ },
112
+ error() {
113
+ return getDirectorDraftError(this.draft, this.rings);
114
+ }
115
+ },
116
+ watch: {
117
+ show: {
118
+ immediate: true,
119
+ handler(value) {
120
+ if (value) this.resetDraft();
121
+ this.$nextTick(this.syncDialog);
122
+ }
123
+ },
124
+ stations: {
125
+ deep: true,
126
+ handler() {
127
+ if (this.show) this.resetDraft();
128
+ }
129
+ },
130
+ availableDirectors: {
131
+ deep: true,
132
+ handler() {
133
+ if (!this.show) return;
134
+ this.draft = reconcileDirectorDraft(this.availableDirectors, this.stations, this.draft);
135
+ }
136
+ }
137
+ },
138
+ mounted() {
139
+ this.syncDialog();
140
+ },
141
+ methods: {
142
+ resetDraft() {
143
+ this.draft = createDirectorDraft(this.availableDirectors, this.stations);
144
+ },
145
+ syncDialog() {
146
+ const dialog = this.$refs.dialog;
147
+ if (!dialog) return;
148
+ if (this.show && !dialog.open) {
149
+ dialog.showModal();
150
+ return;
151
+ }
152
+ if (!this.show && dialog.open) dialog.close();
153
+ },
154
+ close() {
155
+ this.$emit('update:show', false);
156
+ },
157
+ submit() {
158
+ const stations = createDirectorStations(this.draft, this.rings);
159
+ if (!stations) return;
160
+ this.$emit('submit:stations', stations);
161
+ this.close();
162
+ }
163
+ }
164
+ };
165
+ </script>
@@ -47,3 +47,9 @@ export const WithDEBout = {
47
47
  }
48
48
  }
49
49
  };
50
+
51
+ export const WithDirector = {
52
+ args: {
53
+ bout: { ...getPoolsWithBouts[0], RefName: 'Alex Morgan' }
54
+ }
55
+ };
@@ -1,18 +1,18 @@
1
- import { shallowMount } from '@vue/test-utils';
2
- import { describe, expect, it } from 'vitest';
3
- import DirectorModal from './DirectorModal.vue';
4
-
5
- describe('DirectorModal backdrop', () => {
6
- it('keeps the directing screen visible through a 25 percent backdrop', () => {
7
- const wrapper = shallowMount(DirectorModal, {
8
- props: {
9
- show: true,
10
- bout: { BoutId: 42 },
11
- eventRules: {},
12
- hostingClubColors: {}
13
- }
14
- });
15
-
16
- expect(wrapper.find('[data-testid="director-modal-backdrop"]').classes()).toContain('bg-secondary/25');
17
- });
18
- });
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { describe, expect, it } from 'vitest';
3
+ import DirectorModal from './DirectorModal.vue';
4
+
5
+ describe('DirectorModal backdrop', () => {
6
+ it('keeps the directing screen visible through a 25 percent backdrop', () => {
7
+ const wrapper = shallowMount(DirectorModal, {
8
+ props: {
9
+ show: true,
10
+ bout: { BoutId: 42 },
11
+ eventRules: {},
12
+ hostingClubColors: {}
13
+ }
14
+ });
15
+
16
+ expect(wrapper.find('[data-testid="director-modal-backdrop"]').classes()).toContain('bg-secondary/25');
17
+ });
18
+ });
@@ -51,7 +51,7 @@ export default {
51
51
  required: true
52
52
  },
53
53
  eventRules: {
54
- type: Object,
54
+ type: Array,
55
55
  required: true
56
56
  },
57
57
  hostingClubColors: {
@@ -10,7 +10,11 @@
10
10
  @start="onStart"
11
11
  @resume="onResume"
12
12
  @edit="onEdit"
13
- />
13
+ >
14
+ <template v-if="$slots['mobile-details']" #mobile-details>
15
+ <slot name="mobile-details" />
16
+ </template>
17
+ </component>
14
18
  </template>
15
19
 
16
20
  <script>
@@ -51,16 +51,24 @@
51
51
 
52
52
  <!-- Mobile active -->
53
53
  <div
54
- class="flex md:hidden justify-between items-center p-3 border border-alarm rounded-md bg-poolBox transition"
55
- :class="mobileCardActionClass"
54
+ class="flex md:hidden justify-between items-center px-3 py-2 border-alarm rounded-md bg-poolBox transition"
55
+ :class="[mobileCardActionClass, mobileCardLayoutClass]"
56
56
  @click="onCardClick"
57
57
  >
58
- <div class="relative flex flex-col items-center">
58
+ <div class="relative h-12 w-12 flex-none">
59
59
  <img :src="fencer1Portrait" alt="Portrait" class="w-12 h-12 rounded-full" />
60
- <div :class="`w-4 h-4 rounded-full bg-${card.fencer1.colorDot} absolute top-0 left-0`"></div>
60
+ <div :class="`w-4 h-4 rounded-full bg-${card.fencer1.colorDot} absolute bottom-0 -right-1`"></div>
61
+ <span
62
+ class="pointer-events-none absolute -top-1 left-1/2 z-10 -translate-x-1/2 whitespace-nowrap rounded bg-alarm px-1 py-0.5 text-[9px] font-bold leading-none text-white"
63
+ >
64
+ In progress
65
+ </span>
61
66
  </div>
62
67
 
63
- <div class="text-center flex-1 mx-4">
68
+ <div class="text-center flex-1 min-w-0 mx-3">
69
+ <div v-if="$slots['mobile-details']" class="mb-1 min-w-0">
70
+ <slot name="mobile-details" />
71
+ </div>
64
72
  <BaseText :text="`${card.score1} - ${card.fencer1.name}`" size="sm" color="quinary" weight="bold" />
65
73
  <div>
66
74
  <BaseText :text="displayTime" size="xs" color="quaternary" weight="bold" class="-mt-0.5" />
@@ -68,8 +76,8 @@
68
76
  <BaseText :text="`${card.score2} - ${card.fencer2.name}`" size="sm" color="quinary" weight="bold" />
69
77
  </div>
70
78
 
71
- <div class="relative flex flex-col items-center">
72
- <div :class="`w-4 h-4 rounded-full bg-${card.fencer2.colorDot} absolute top-0 right-0`"></div>
79
+ <div class="relative h-12 w-12 flex-none">
80
+ <div :class="`w-4 h-4 rounded-full bg-${card.fencer2.colorDot} absolute bottom-0 -right-1`"></div>
73
81
  <img :src="fencer2Portrait" alt="Portrait" class="w-12 h-12 rounded-full" />
74
82
  </div>
75
83
  </div>
@@ -101,6 +109,9 @@ export default {
101
109
  hasCard() {
102
110
  return !!this.card;
103
111
  },
112
+ mobileCardLayoutClass() {
113
+ return this.$slots['mobile-details'] ? 'border-2' : 'border';
114
+ },
104
115
  mobileCardActionClass() {
105
116
  if (this.editMode) return 'cursor-pointer';
106
117
  return 'cursor-default';
@@ -131,9 +142,7 @@ export default {
131
142
  'card.timeLeftSeconds': {
132
143
  immediate: true,
133
144
  handler(n) {
134
- const next = Number(n ?? 0);
135
- if (this.timerStatus !== 'running') this.remaining = next;
136
- if (this.timerStatus === 'running' && this.remaining === 0) this.remaining = next;
145
+ this.remaining = Math.max(0, Math.floor(Number(n) || 0));
137
146
  }
138
147
  },
139
148
  timerStatus(n) {
@@ -59,15 +59,16 @@
59
59
 
60
60
  <!-- Mobile -->
61
61
  <section
62
- class="relative flex md:hidden items-center border rounded-md bg-poolBox transition overflow-hidden"
62
+ class="relative flex flex-col md:hidden items-center border rounded-md bg-poolBox transition overflow-hidden"
63
63
  :class="mobileCardActionClass"
64
+ @click="onCardClick"
64
65
  >
65
66
  <!-- main tappable area -->
66
- <div class="flex flex-1 items-center p-4 min-w-0" @click="onCardClick">
67
+ <div class="flex w-full flex-1 items-center px-3 py-2 min-w-0">
67
68
  <!-- left portrait or index when reordering -->
68
- <div v-if="!reorderMode" class="relative flex flex-col items-center flex-none">
69
+ <div v-if="!reorderMode" class="relative w-12 h-12 flex-none">
69
70
  <img :src="fencer1Portrait" alt="Portrait" class="w-12 h-12 rounded-full" />
70
- <div :class="`w-4 h-4 rounded-full bg-${card.fencer1.colorDot} relative -mt-4 ml-9 z-10`"></div>
71
+ <div :class="`w-4 h-4 rounded-full bg-${card.fencer1.colorDot} absolute bottom-0 -right-1 z-10`"></div>
71
72
  </div>
72
73
 
73
74
  <div v-if="reorderMode" class="flex flex-col items-center -mt-3 select-none flex-none">
@@ -76,8 +77,11 @@
76
77
  </div>
77
78
 
78
79
  <!-- center -->
79
- <div class="text-center flex-1 mx-4 min-w-0 flex flex-col items-center justify-center leading-none">
80
- <div class="flex items-center justify-center">
80
+ <div class="text-center flex-1 mx-3 min-w-0 flex flex-col items-center justify-center leading-none">
81
+ <div v-if="$slots['mobile-details']" class="mb-1 w-full min-w-0">
82
+ <slot name="mobile-details" />
83
+ </div>
84
+ <div class="flex max-w-full items-center justify-center">
81
85
  <div :class="`mr-2 bg-${card.fencer1.colorDot} w-3 h-3 flex-none`"></div>
82
86
  <BaseText
83
87
  :text="c.fencer1.name"
@@ -90,7 +94,7 @@
90
94
 
91
95
  <BaseText text="vs" size="xs" color="quaternary" />
92
96
 
93
- <div class="flex items-center justify-center">
97
+ <div class="flex max-w-full items-center justify-center">
94
98
  <div :class="`mr-2 bg-${card.fencer2.colorDot} w-3 h-3 flex-none`"></div>
95
99
  <BaseText
96
100
  :text="c.fencer2.name"
@@ -1,33 +1,42 @@
1
- import BracketDirectorAssignmentCard from './BracketDirectorAssignmentCard.vue';
2
-
3
- export default {
4
- title: 'Organisms/Cards/BracketDirectorAssignmentCard',
5
- component: BracketDirectorAssignmentCard,
6
- args: {
7
- station: {
8
- RingId: 1,
9
- RingName: 'Ring 1',
10
- Color: '#2563eb',
11
- Director: {
12
- PersonId: 1001,
13
- DisplayName: 'Alex Morgan',
14
- PortraitUrl: ''
15
- }
16
- },
17
- variant: 'assigned'
18
- },
19
- argTypes: {
20
- station: { control: 'object' },
21
- variant: { control: 'select', options: ['assigned', 'add'] },
22
- addLabel: { control: 'text' }
23
- }
24
- };
25
-
26
- export const Assigned = {};
27
-
28
- export const AddDirector = {
29
- args: {
30
- station: null,
31
- variant: 'add'
32
- }
33
- };
1
+ import BracketDirectorAssignmentCard from './BracketDirectorAssignmentCard.vue';
2
+
3
+ export default {
4
+ title: 'Organisms/Cards/BracketDirectorAssignmentCard',
5
+ component: BracketDirectorAssignmentCard,
6
+ args: {
7
+ station: {
8
+ RingId: 1,
9
+ RingName: 'Ring 1',
10
+ Color: '#2563eb',
11
+ Director: {
12
+ PersonId: 1001,
13
+ DisplayName: 'Alex Morgan',
14
+ PortraitUrl: ''
15
+ }
16
+ },
17
+ variant: 'assigned'
18
+ },
19
+ argTypes: {
20
+ station: { control: 'object' },
21
+ variant: { control: 'select', options: ['assigned', 'add'] },
22
+ addLabel: { control: 'text' }
23
+ }
24
+ };
25
+
26
+ export const Assigned = {
27
+ parameters: {
28
+ docs: {
29
+ description: {
30
+ story:
31
+ 'The remove control emits action:remove with a detached station payload. The parent updates planned coverage.'
32
+ }
33
+ }
34
+ }
35
+ };
36
+
37
+ export const AddDirector = {
38
+ args: {
39
+ station: null,
40
+ variant: 'add'
41
+ }
42
+ };
@@ -1,45 +1,76 @@
1
- import { shallowMount } from '@vue/test-utils';
2
- import { describe, expect, it } from 'vitest';
3
- import BracketDirectorAssignmentCard from './BracketDirectorAssignmentCard.vue';
4
-
5
- const station = {
6
- RingId: 3,
7
- RingName: 'Ring 3',
8
- Color: '#059669',
9
- Director: { PersonId: 1003, DisplayName: 'Sam Rivera', PortraitUrl: '/portrait.png' }
10
- };
11
-
12
- describe('BracketDirectorAssignmentCard', () => {
13
- it('renders the ring, portrait, and director name', () => {
14
- const wrapper = shallowMount(BracketDirectorAssignmentCard, { props: { station } });
15
-
16
- expect(wrapper.find('img').attributes()).toMatchObject({
17
- src: '/portrait.png',
18
- alt: 'Sam Rivera portrait'
19
- });
20
- expect(wrapper.findAll('base-text-stub').map(text => text.attributes('text'))).toContain('Sam Rivera');
21
- expect(wrapper.find('[data-testid="bracket-director-assignment"]').attributes('style')).toBeUndefined();
22
- expect(wrapper.find('[data-testid="director-color-dot"]').attributes('style')).toContain('#059669');
23
- expect(wrapper.find('[data-testid="ring-number-icon"]').exists()).toBe(false);
24
- expect(wrapper.findAll('base-text-stub').map(text => text.attributes('text'))).toContain('Ring 3');
25
- });
26
-
27
- it('uses director initials when no portrait is supplied', () => {
28
- const wrapper = shallowMount(BracketDirectorAssignmentCard, {
29
- props: { station: { ...station, Director: { ...station.Director, PortraitUrl: '' } } }
30
- });
31
-
32
- expect(wrapper.find('img').exists()).toBe(false);
33
- expect(wrapper.find('[data-testid="director-initials"]').classes()).toContain('rounded-xl');
34
- expect(wrapper.find('[data-testid="director-initials"] base-text-stub').attributes('text')).toBe('SR');
35
- });
36
-
37
- it('emits add intent from the summary card', async () => {
38
- const wrapper = shallowMount(BracketDirectorAssignmentCard, { props: { variant: 'add' } });
39
-
40
- await wrapper.find('[data-testid="add-bracket-director"]').trigger('click');
41
-
42
- expect(wrapper.emitted('action:add')).toEqual([[]]);
43
- expect(wrapper.find('base-text-stub').attributes('text')).toBe('Click here to add director');
44
- });
45
- });
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { describe, expect, it } from 'vitest';
3
+ import BracketDirectorAssignmentCard from './BracketDirectorAssignmentCard.vue';
4
+
5
+ const station = {
6
+ RingId: 3,
7
+ RingName: 'Ring 3',
8
+ Color: '#059669',
9
+ Director: { PersonId: 1003, DisplayName: 'Sam Rivera', PortraitUrl: '/portrait.png' }
10
+ };
11
+
12
+ describe('BracketDirectorAssignmentCard', () => {
13
+ it('renders the ring, portrait, and director name', () => {
14
+ const wrapper = shallowMount(BracketDirectorAssignmentCard, { props: { station } });
15
+
16
+ expect(wrapper.find('img').attributes()).toMatchObject({
17
+ src: '/portrait.png',
18
+ alt: 'Sam Rivera portrait'
19
+ });
20
+ expect(wrapper.findAll('base-text-stub').map(text => text.attributes('text'))).toContain('Sam Rivera');
21
+ expect(wrapper.find('[data-testid="bracket-director-assignment"]').attributes('style')).toBeUndefined();
22
+ expect(wrapper.find('[data-testid="director-color-dot"]').attributes('style')).toContain('#059669');
23
+ expect(wrapper.find('[data-testid="ring-number-icon"]').exists()).toBe(false);
24
+ expect(wrapper.findAll('base-text-stub').map(text => text.attributes('text'))).toContain('Ring 3');
25
+ });
26
+
27
+ it('uses director initials when no portrait is supplied', () => {
28
+ const wrapper = shallowMount(BracketDirectorAssignmentCard, {
29
+ props: { station: { ...station, Director: { ...station.Director, PortraitUrl: '' } } }
30
+ });
31
+
32
+ expect(wrapper.find('img').exists()).toBe(false);
33
+ expect(wrapper.find('[data-testid="director-initials"]').classes()).toContain('rounded-xl');
34
+ expect(wrapper.find('[data-testid="director-initials"] base-text-stub').attributes('text')).toBe('SR');
35
+ });
36
+
37
+ it('emits add intent from the summary card', async () => {
38
+ const wrapper = shallowMount(BracketDirectorAssignmentCard, { props: { variant: 'add' } });
39
+
40
+ await wrapper.find('[data-testid="add-bracket-director"]').trigger('click');
41
+
42
+ expect(wrapper.emitted('action:add')).toEqual([[]]);
43
+ expect(wrapper.find('base-text-stub').attributes('text')).toBe('Click here to add director');
44
+ });
45
+
46
+ it('emits a detached station when removing a director and waits for updated props', async () => {
47
+ const selectedStation = structuredClone(station);
48
+ const wrapper = shallowMount(BracketDirectorAssignmentCard, { props: { station: selectedStation } });
49
+ const removeButton = wrapper.get('[data-testid="remove-bracket-director"]');
50
+ expect(removeButton.attributes('aria-label')).toBe('Remove Sam Rivera from Ring 3');
51
+ await removeButton.trigger('click');
52
+ const payload = wrapper.emitted('action:remove')[0][0];
53
+ expect(payload).toEqual(selectedStation);
54
+ expect(payload).not.toBe(selectedStation);
55
+ payload.Director.DisplayName = 'Changed';
56
+ expect(selectedStation.Director.DisplayName).toBe('Sam Rivera');
57
+ expect(wrapper.find('[data-testid="bracket-director-assignment"]').exists()).toBe(true);
58
+ await wrapper.setProps({ station: null });
59
+ expect(wrapper.find('[data-testid="bracket-director-assignment"]').exists()).toBe(false);
60
+ });
61
+
62
+ it('has no remove control on the add card or an empty card', () => {
63
+ const addCard = shallowMount(BracketDirectorAssignmentCard, { props: { variant: 'add' } });
64
+ const emptyCard = shallowMount(BracketDirectorAssignmentCard);
65
+ expect(addCard.find('[data-testid="remove-bracket-director"]').exists()).toBe(false);
66
+ expect(emptyCard.find('[data-testid="remove-bracket-director"]').exists()).toBe(false);
67
+ });
68
+
69
+ it('displays the ring as text and reflects parent updates', async () => {
70
+ const wrapper = shallowMount(BracketDirectorAssignmentCard, { props: { station } });
71
+ expect(wrapper.find('select').exists()).toBe(false);
72
+ expect(wrapper.findAll('base-text-stub').map(text => text.attributes('text'))).toContain('Ring 3');
73
+ await wrapper.setProps({ station: { ...station, RingId: 1, RingName: 'Ring 1' } });
74
+ expect(wrapper.findAll('base-text-stub').map(text => text.attributes('text'))).toContain('Ring 1');
75
+ });
76
+ });