@dcrackel/hematournamentui 1.0.707 → 1.0.708

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 (46) hide show
  1. package/AGENTS.md +118 -118
  2. package/dist/HemaTournamentUI-lib.es.js +4483 -4618
  3. package/dist/HemaTournamentUI-lib.umd.js +27 -27
  4. package/dist/hematournamentui-tailwind.css +185 -406
  5. package/package.json +10 -10
  6. package/src/index.js +2 -0
  7. package/src/mocks/getDEBouts-42Fencers.js +4 -4
  8. package/src/mocks/getDEBouts-TableOf128.js +37 -0
  9. package/src/mocks/getDEBouts-TableOf64.js +35 -0
  10. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.stories.js +23 -0
  11. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.vue +35 -0
  12. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.test.js +18 -0
  13. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.vue +3 -2
  14. package/src/stories/Molecules/Modals/EditBoutModal/EditBoutModal.vue +1 -1
  15. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +33 -0
  16. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +45 -0
  17. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +103 -0
  18. package/src/stories/Organisms/Cards/FencerCard/FencerCard.vue +298 -278
  19. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.stories.js +60 -98
  20. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.test.js +103 -4
  21. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.vue +256 -215
  22. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.stories.js +69 -92
  23. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.vue +80 -128
  24. package/src/stories/Organisms/Cards/WaitlistFencerCard/WaitlistFencerCard.vue +14 -4
  25. package/src/stories/Organisms/Containers/Tableau/Tableau.stories.js +2 -2
  26. package/src/stories/Organisms/Containers/Tableau/Tableau.test.js +131 -3
  27. package/src/stories/Organisms/Containers/Tableau/Tableau.vue +287 -89
  28. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +29 -0
  29. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +46 -0
  30. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.test.js +21 -0
  31. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.vue +39 -0
  32. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.js +145 -0
  33. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.test.js +64 -0
  34. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.js +57 -0
  35. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.test.js +65 -0
  36. package/src/stories/Organisms/Containers/TableauColumn/TableauColumn.vue +31 -29
  37. package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +146 -18
  38. package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +52 -2
  39. package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +53 -17
  40. package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +39 -0
  41. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.js +39 -0
  42. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.test.js +79 -0
  43. package/src/stories/Organisms/Cards/TableauBoutCard/SpacingConfig.js +0 -111
  44. package/src/stories/Organisms/Containers/TableauLines/SpacingConfig.js +0 -162
  45. package/src/stories/Organisms/Containers/TableauLines/TableauLines.stories.js +0 -21
  46. package/src/stories/Organisms/Containers/TableauLines/TableauLines.vue +0 -78
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dcrackel/hematournamentui",
3
3
  "private": false,
4
- "version": "1.0.707",
4
+ "version": "1.0.708",
5
5
  "type": "module",
6
6
  "main": "dist/HemaTournamentUI-lib.umd.js",
7
7
  "module": "dist/HemaTournamentUI-lib.es.js",
@@ -58,18 +58,18 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@babel/eslint-parser": "^7.28.6",
61
- "@chromatic-com/storybook": "^5.3.0",
61
+ "@chromatic-com/storybook": "^5.5.1",
62
62
  "@eslint/eslintrc": "^3.3.5",
63
- "@storybook/addon-docs": "^10.5.6",
64
- "@storybook/addon-links": "^10.5.6",
65
- "@storybook/react": "^10.5.6",
66
- "@storybook/vue3-vite": "^10.5.6",
63
+ "@storybook/addon-docs": "^10.6.2",
64
+ "@storybook/addon-links": "^10.6.2",
65
+ "@storybook/react": "^10.6.2",
66
+ "@storybook/vue3-vite": "^10.6.2",
67
67
  "@tailwindcss/cli": "^4.3.0",
68
68
  "@tailwindcss/postcss": "^4.3.0",
69
69
  "@typescript-eslint/eslint-plugin": "^8.59.3",
70
70
  "@typescript-eslint/parser": "^8.59.3",
71
71
  "@vitejs/plugin-vue": "^6.0.7",
72
- "@vitest/coverage-v8": "^4.1.6",
72
+ "@vitest/coverage-v8": "^4.1.11",
73
73
  "@vue/test-utils": "^2.4.10",
74
74
  "@vuepic/vue-datepicker": "^12.1.0",
75
75
  "autoprefixer": "^10.5.0",
@@ -77,17 +77,17 @@
77
77
  "eslint": "^9.9.0",
78
78
  "eslint-plugin-jest": "^29.15.2",
79
79
  "eslint-plugin-prettier": "^5.5.5",
80
- "eslint-plugin-storybook": "^10.5.6",
80
+ "eslint-plugin-storybook": "^10.6.2",
81
81
  "eslint-plugin-vue": "^10.9.1",
82
82
  "happy-dom": "^20.9.0",
83
83
  "patch-package": "^8.0.1",
84
84
  "postcss": "^8.5.14",
85
85
  "prettier": "^3.9.6",
86
- "storybook": "^10.5.6",
86
+ "storybook": "^10.6.2",
87
87
  "storybook-vue3-router": "^7.0.0",
88
88
  "tailwindcss": "^4.3.0",
89
89
  "vite": "^8.0.13",
90
- "vitest": "^4.1.6",
90
+ "vitest": "^4.1.11",
91
91
  "vue-eslint-parser": "^10.4.0"
92
92
  },
93
93
  "overrides": {
package/src/index.js CHANGED
@@ -17,6 +17,8 @@ export { default as DatePicker } from './stories/Organisms/ComplexInputs/DatePic
17
17
  export { default as DropDownMenu } from './stories/Organisms/ComplexInputs/DropDown/DropDownMenu.vue';
18
18
  export { default as GridContainer } from './stories/Organisms/Grids/GridContainer.vue';
19
19
  export { default as PoolGrid } from './stories/Organisms/Containers/PoolGrid/PoolGrid.vue';
20
+ export { default as BracketDirectorAssignmentCard } from './stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue';
21
+ export { default as BracketDirectorAssignmentPanel } from './stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue';
20
22
  export { default as ImageCropper } from './stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue';
21
23
  export { default as TextBoxEditor } from './stories/Organisms/ComplexInputs/TextBoxEditor/TextBoxEditor.vue';
22
24
  export { default as EditEventWizard } from './stories/Organisms/Wizards/EditEventWizard/EditEventWizard.vue';
@@ -3150,7 +3150,7 @@ const getDEWithBouts42Fencers = {
3150
3150
  TimeLeft: 0,
3151
3151
  CurrentPass: 0,
3152
3152
  MaxPasses: 0,
3153
- RoundLabel: 'Quarterfinal',
3153
+ RoundLabel: 'Table of 8',
3154
3154
  DEBoutId: 57,
3155
3155
  DENextBoutId: 61,
3156
3156
  LoserNextBoutId: null,
@@ -3201,7 +3201,7 @@ const getDEWithBouts42Fencers = {
3201
3201
  TimeLeft: 0,
3202
3202
  CurrentPass: 0,
3203
3203
  MaxPasses: 0,
3204
- RoundLabel: 'Quarterfinal',
3204
+ RoundLabel: 'Table of 8',
3205
3205
  DEBoutId: 58,
3206
3206
  DENextBoutId: 61,
3207
3207
  LoserNextBoutId: null,
@@ -3252,7 +3252,7 @@ const getDEWithBouts42Fencers = {
3252
3252
  TimeLeft: 0,
3253
3253
  CurrentPass: 0,
3254
3254
  MaxPasses: 0,
3255
- RoundLabel: 'Quarterfinal',
3255
+ RoundLabel: 'Table of 8',
3256
3256
  DEBoutId: 59,
3257
3257
  DENextBoutId: 62,
3258
3258
  LoserNextBoutId: null,
@@ -3303,7 +3303,7 @@ const getDEWithBouts42Fencers = {
3303
3303
  TimeLeft: 0,
3304
3304
  CurrentPass: 0,
3305
3305
  MaxPasses: 0,
3306
- RoundLabel: 'Quarterfinal',
3306
+ RoundLabel: 'Table of 8',
3307
3307
  DEBoutId: 60,
3308
3308
  DENextBoutId: 62,
3309
3309
  LoserNextBoutId: null,
@@ -0,0 +1,37 @@
1
+ import historicalSixtySevenFencerBracket from './getDEBouts-67Fencers.js';
2
+
3
+ const roundLabelRules = [
4
+ { matches: boutId => boutId >= 1 && boutId <= 64, label: 'Table of 128' },
5
+ { matches: boutId => boutId >= 65 && boutId <= 96, label: 'Table of 64' },
6
+ { matches: boutId => boutId >= 97 && boutId <= 112, label: 'Table of 32' },
7
+ { matches: boutId => boutId >= 113 && boutId <= 120, label: 'Table of 16' },
8
+ { matches: boutId => boutId >= 121 && boutId <= 124, label: 'Table of 8' },
9
+ { matches: boutId => boutId >= 125 && boutId <= 126, label: 'Semifinal' },
10
+ { matches: boutId => boutId === 127, label: 'Final' }
11
+ ];
12
+
13
+ const getCurrentRoundLabel = bout => {
14
+ const boutId = Number(bout.DEBoutId);
15
+ return roundLabelRules.find(rule => rule.matches(boutId))?.label || bout.RoundLabel;
16
+ };
17
+
18
+ const normalizeTableOf128Bout = bout => ({
19
+ ...bout,
20
+ RoundLabel: getCurrentRoundLabel(bout)
21
+ });
22
+
23
+ const [historicalPool, ...otherPools] = historicalSixtySevenFencerBracket.pools;
24
+
25
+ const getDEBoutsTableOf128 = {
26
+ ...historicalSixtySevenFencerBracket,
27
+ message: 'Current Table of 128 fixture retrieved',
28
+ pools: [
29
+ {
30
+ ...historicalPool,
31
+ Bouts: historicalPool.Bouts.map(normalizeTableOf128Bout)
32
+ },
33
+ ...otherPools
34
+ ]
35
+ };
36
+
37
+ export default getDEBoutsTableOf128;
@@ -0,0 +1,35 @@
1
+ import historicalSixtySevenFencerBracket from './getDEBouts-67Fencers.js';
2
+
3
+ const FIRST_TABLE_OF_64_BOUT_ID = 65;
4
+
5
+ const roundLabelRules = [
6
+ { matches: boutId => boutId >= 65 && boutId <= 96, label: 'Table of 64' },
7
+ { matches: boutId => boutId >= 97 && boutId <= 112, label: 'Table of 32' },
8
+ { matches: boutId => boutId >= 113 && boutId <= 120, label: 'Table of 16' },
9
+ { matches: boutId => boutId >= 121 && boutId <= 124, label: 'Table of 8' },
10
+ { matches: boutId => boutId >= 125 && boutId <= 126, label: 'Semifinal' },
11
+ { matches: boutId => boutId === 127, label: 'Final' }
12
+ ];
13
+
14
+ const isTableOf64Bout = bout => Number(bout.DEBoutId) >= FIRST_TABLE_OF_64_BOUT_ID;
15
+
16
+ const getCurrentRoundLabel = bout => {
17
+ const boutId = Number(bout.DEBoutId);
18
+ return roundLabelRules.find(rule => rule.matches(boutId))?.label || bout.RoundLabel;
19
+ };
20
+
21
+ const normalizeTableOf64Bout = bout => ({
22
+ ...bout,
23
+ RoundLabel: getCurrentRoundLabel(bout)
24
+ });
25
+
26
+ const [historicalPool, ...otherPools] = historicalSixtySevenFencerBracket.pools;
27
+ const tableOf64Bouts = historicalPool.Bouts.filter(isTableOf64Bout).map(normalizeTableOf64Bout);
28
+
29
+ const getDEBoutsTableOf64 = {
30
+ ...historicalSixtySevenFencerBracket,
31
+ message: 'Current Table of 64 fixture retrieved',
32
+ pools: [{ ...historicalPool, Bouts: tableOf64Bouts }, ...otherPools]
33
+ };
34
+
35
+ export default getDEBoutsTableOf64;
@@ -0,0 +1,23 @@
1
+ import MissingHemaRatingsId from './MissingHemaRatingsId.vue';
2
+
3
+ export default {
4
+ title: 'Molecules/Indicators/MissingHemaRatingsId',
5
+ component: MissingHemaRatingsId,
6
+ tags: ['autodocs'],
7
+ args: { person: { HemaRatingsId: null } },
8
+ argTypes: {
9
+ person: { control: 'object' }
10
+ }
11
+ };
12
+
13
+ export const MissingId = {
14
+ args: { person: { HemaRatingsId: null } }
15
+ };
16
+
17
+ export const UnratedWithId = {
18
+ args: { person: { HemaRatingsId: 10255, HEMARatings: [] } }
19
+ };
20
+
21
+ export const IdFromRating = {
22
+ args: { person: { HEMARatings: [{ HemaRatingId: 10255, Rating: 1708.8 }] } }
23
+ };
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <span
3
+ v-if="!hasHemaRatingsId"
4
+ role="img"
5
+ aria-label="Missing HEMA Ratings ID"
6
+ title="Missing HEMA Ratings ID"
7
+ data-testid="missing-hema-ratings-id"
8
+ class="inline-flex shrink-0 items-center justify-center text-alarm"
9
+ >
10
+ <svg class="h-6 w-6 md:h-7 md:w-7" viewBox="0 0 32 32" fill="none" aria-hidden="true">
11
+ <circle cx="16" cy="16" r="13" stroke="currentColor" stroke-width="2" />
12
+ <text x="16" y="20" text-anchor="middle" fill="currentColor" class="font-bold" font-size="11">HR</text>
13
+ <path d="M7 25 25 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
14
+ </svg>
15
+ </span>
16
+ </template>
17
+
18
+ <script>
19
+ export default {
20
+ name: 'MissingHemaRatingsId',
21
+ props: {
22
+ person: {
23
+ type: Object,
24
+ default: null
25
+ }
26
+ },
27
+ computed: {
28
+ hasHemaRatingsId() {
29
+ const id = this.person?.HemaRatingsId ?? this.person?.HEMARatings?.[0]?.HemaRatingId;
30
+ const numericId = Number(id);
31
+ return Number.isInteger(numericId) && numericId > 0;
32
+ }
33
+ }
34
+ };
35
+ </script>
@@ -0,0 +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
+ });
@@ -9,10 +9,11 @@
9
9
  >
10
10
  <div
11
11
  v-if="show"
12
- class="fixed inset-0 bg-secondary bg-opacity-75 flex items-center justify-center z-50"
12
+ data-testid="director-modal-backdrop"
13
+ class="fixed inset-0 z-50 flex items-center justify-center bg-secondary/85"
13
14
  @click="close"
14
15
  >
15
- <div class="w-full md:w-1/3 md:min-w-[30rem] bg-neutral rounded-lg shadow-lg pb-6 px-2" @click.stop>
16
+ <div class="w-full md:w-1/3 md:min-w-120 bg-neutral rounded-lg shadow-lg pb-6 px-2" @click.stop>
16
17
  <div class="flex w-full justify-end mt-1">
17
18
  <BaseIcon icon-name="fa-circle-xmark" size="lg" color="quaternary" hover="alarm" @click="close" />
18
19
  </div>
@@ -47,7 +47,7 @@ export default {
47
47
  required: true
48
48
  },
49
49
  eventRules: {
50
- type: Object,
50
+ type: Array,
51
51
  required: true
52
52
  },
53
53
  hostingClubColors: {
@@ -0,0 +1,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
+
28
+ export const AddDirector = {
29
+ args: {
30
+ station: null,
31
+ variant: 'add'
32
+ }
33
+ };
@@ -0,0 +1,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
+ });
@@ -0,0 +1,103 @@
1
+ <template>
2
+ <button
3
+ v-if="isAddCard"
4
+ type="button"
5
+ data-testid="add-bracket-director"
6
+ class="flex min-h-20 w-full items-center justify-center gap-3 rounded-lg border-2 border-dashed border-dropdownSelect bg-white px-4 py-3 text-left transition hover:border-bright hover:shadow-md focus-visible:border-bright focus-visible:outline-none"
7
+ @click="$emit('action:add')"
8
+ >
9
+ <span class="flex h-9 w-9 items-center justify-center rounded-full bg-bannerBackground" aria-hidden="true">
10
+ <BaseIcon iconName="fa-plus" color="primaryHighlight" size="sm" />
11
+ </span>
12
+ <BaseText :text="addLabel" color="primaryHighlight" size="sm" weight="bold" />
13
+ </button>
14
+
15
+ <article
16
+ v-else-if="station"
17
+ data-testid="bracket-director-assignment"
18
+ class="flex min-h-16 w-full items-center gap-3 overflow-hidden rounded-md border border-poolSetup bg-white px-3 py-2 shadow-sm hover:border-bright"
19
+ :aria-label="assignmentLabel"
20
+ >
21
+ <div class="relative h-12 w-12 flex-none">
22
+ <img
23
+ v-if="portraitUrl"
24
+ :src="portraitUrl"
25
+ :alt="`${directorName} portrait`"
26
+ class="h-12 w-12 rounded-xl object-cover"
27
+ />
28
+ <div
29
+ v-else
30
+ data-testid="director-initials"
31
+ class="flex h-12 w-12 items-center justify-center rounded-xl bg-dropdownSelect"
32
+ aria-hidden="true"
33
+ >
34
+ <BaseText :text="directorInitials" color="primaryHighlight" size="sm" weight="bold" />
35
+ </div>
36
+ <span
37
+ data-testid="director-color-dot"
38
+ class="absolute -bottom-1 -right-1 h-4 w-4 rounded-full ring-2 ring-white"
39
+ :style="{ backgroundColor: station.Color }"
40
+ aria-hidden="true"
41
+ ></span>
42
+ </div>
43
+
44
+ <div class="min-w-0 flex-1">
45
+ <BaseText :text="ringLabel" color="quaternary" size="xs" />
46
+ <BaseText class="truncate" :text="directorName" color="primaryHighlight" size="sm" weight="bold" />
47
+ </div>
48
+ </article>
49
+ </template>
50
+
51
+ <script>
52
+ import BaseIcon from '../../../Atoms/Icon/BaseIcon.vue';
53
+ import BaseText from '../../../Atoms/Text/BaseText.vue';
54
+
55
+ const getInitials = displayName =>
56
+ String(displayName || '')
57
+ .split(/\s+/)
58
+ .filter(Boolean)
59
+ .slice(0, 2)
60
+ .map(name => name[0].toUpperCase())
61
+ .join('');
62
+
63
+ export default {
64
+ name: 'BracketDirectorAssignmentCard',
65
+ components: { BaseIcon, BaseText },
66
+ emits: ['action:add'],
67
+ props: {
68
+ station: {
69
+ type: Object,
70
+ default: null
71
+ },
72
+ variant: {
73
+ type: String,
74
+ default: 'assigned',
75
+ validator: value => ['assigned', 'add'].includes(value)
76
+ },
77
+ addLabel: {
78
+ type: String,
79
+ default: 'Click here to add director'
80
+ }
81
+ },
82
+ computed: {
83
+ assignmentLabel() {
84
+ return `${this.ringLabel}, Director ${this.directorName}`;
85
+ },
86
+ directorInitials() {
87
+ return getInitials(this.directorName) || '?';
88
+ },
89
+ directorName() {
90
+ return this.station?.Director?.DisplayName || 'Director TBD';
91
+ },
92
+ isAddCard() {
93
+ return this.variant === 'add';
94
+ },
95
+ portraitUrl() {
96
+ return this.station?.Director?.PortraitUrl || '';
97
+ },
98
+ ringLabel() {
99
+ return this.station?.RingName || `Ring ${this.station?.RingId ?? ''}`.trim();
100
+ }
101
+ }
102
+ };
103
+ </script>