@eturnity/eturnity_reusable_components 9.19.3 → 9.19.5

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 (51) hide show
  1. package/package.json +4 -1
  2. package/src/DemoChart.vue +16 -16
  3. package/src/assets/svgIcons/3d_house.svg +3 -0
  4. package/src/assets/svgIcons/activities_app.svg +3 -0
  5. package/src/assets/svgIcons/apps_navigation.svg +3 -0
  6. package/src/assets/svgIcons/chevron_down.svg +3 -0
  7. package/src/assets/svgIcons/chevron_up.svg +3 -0
  8. package/src/assets/svgIcons/collapse_sidebar.svg +3 -0
  9. package/src/assets/svgIcons/consumption_profile.svg +3 -0
  10. package/src/assets/svgIcons/cross_filled.svg +3 -0
  11. package/src/assets/svgIcons/data_transfer.svg +2 -2
  12. package/src/assets/svgIcons/eraser.svg +1 -0
  13. package/src/assets/svgIcons/esdec.svg +3 -0
  14. package/src/assets/svgIcons/expand_sidebar.svg +3 -0
  15. package/src/assets/svgIcons/folder_unfilled.svg +3 -0
  16. package/src/assets/svgIcons/freedraw.svg +1 -0
  17. package/src/assets/svgIcons/leads_app.svg +9 -0
  18. package/src/assets/svgIcons/library_app.svg +3 -0
  19. package/src/assets/svgIcons/line.svg +1 -0
  20. package/src/assets/svgIcons/mounting_system.svg +2 -2
  21. package/src/assets/svgIcons/polyline.svg +3 -0
  22. package/src/assets/svgIcons/projects_app.svg +9 -0
  23. package/src/assets/svgIcons/question_mark.svg +2 -2
  24. package/src/assets/svgIcons/recurring_costs.svg +3 -0
  25. package/src/assets/svgIcons/settings.svg +8 -2
  26. package/src/assets/svgIcons/shading_snow.svg +3 -0
  27. package/src/assets/svgIcons/signature.svg +3 -0
  28. package/src/assets/svgIcons/tariff_menu.svg +3 -0
  29. package/src/assets/svgIcons/zev_community_solar.svg +3 -0
  30. package/src/components/banner/actionBanner/index.vue +1 -1
  31. package/src/components/barchart/BottomFields.vue +5 -2
  32. package/src/components/barchart/ChartControls.vue +5 -5
  33. package/src/components/barchart/index.vue +7 -2
  34. package/src/components/buttons/buttonIcon/index.vue +9 -0
  35. package/src/components/buttons/mainButton/index.vue +26 -26
  36. package/src/components/buttons/splitButtons/index.vue +1 -1
  37. package/src/components/dropdown/index.vue +7 -7
  38. package/src/components/errorMessage/index.vue +1 -1
  39. package/src/components/inputs/searchInput/SearchInput.stories.js +1 -1
  40. package/src/components/inputs/searchInput/searchInput.spec.js +1 -1
  41. package/src/components/inputs/select/index.vue +406 -79
  42. package/src/components/inputs/select/option/index.vue +9 -1
  43. package/src/components/modals/cookieConsent/CookieConsent.vue +4 -4
  44. package/src/components/navigationSideMenu/index.vue +1098 -0
  45. package/src/components/pageSubtitle/index.vue +1 -1
  46. package/src/components/projectMarker/ProjectMarker.stories.js +26 -36
  47. package/src/components/projectMarker/index.vue +172 -93
  48. package/src/components/tabsHeader/index.vue +9 -1
  49. package/src/components/videoThumbnail/index.vue +1 -1
  50. package/src/helpers/cookieHelper.js +23 -13
  51. package/src/helpers/dateTimeFormatting.js +0 -1
@@ -0,0 +1,1098 @@
1
+ <template>
2
+ <PageWrapper
3
+ :expanded-width="expandedWidth"
4
+ :is-collapsed="isCollapsed"
5
+ @mouseenter="isWrapperHovered = true"
6
+ @mouseleave="isWrapperHovered = false"
7
+ >
8
+ <SizerRef ref="sizerRef">
9
+ <HeaderSection :is-collapsed="isCollapsed">
10
+ <TopMenuRow>
11
+ <TitleContainer ref="titleRowRef" :is-collapsed="isCollapsed">
12
+ <TitleText
13
+ v-if="headerTitle && !isCollapsed && !showProjectManagerSelect"
14
+ :title="headerTitle"
15
+ >
16
+ {{ headerTitle }}
17
+ </TitleText>
18
+ <ResponsibleBlock v-if="showProjectManagerSelect && !isCollapsed">
19
+ <ResponsibleCard>
20
+ <ResponsibleSelectSurface
21
+ :class="{
22
+ 'ResponsibleSelectSurface--open':
23
+ responsibleSelectSurfaceOpen,
24
+ }"
25
+ >
26
+ <SelectComponent
27
+ align-items="horizontal"
28
+ button-bg-color="transparent"
29
+ caret-icons="sidebar"
30
+ data-id="sidebar_responsible"
31
+ data-qa-id="sidebar_responsible"
32
+ :disabled="projectManagerDisabled"
33
+ :show-disabled-background="false"
34
+ dropdown-bg-color="purple1"
35
+ :dropdown-match-max-content="true"
36
+ :dropdown-search-hide-unassigned="true"
37
+ font-size="13px"
38
+ :has-select-button-padding="false"
39
+ :is-searchable="true"
40
+ :min-option-length="1"
41
+ :search-placeholder="$gettext('Search text')"
42
+ search-placement="dropdown"
43
+ select-height="56px"
44
+ select-min-height="56px"
45
+ select-width="100%"
46
+ :show-border="false"
47
+ :value="projectManagerId"
48
+ @input-change="onProjectManagerInputChange"
49
+ @on-dropdown-close="onResponsibleSelectDropdownClose"
50
+ @on-dropdown-open="onResponsibleSelectDropdownOpen"
51
+ >
52
+ <template #selector>
53
+ <ResponsibleSelectorRow>
54
+ <ResponsibleAvatar>{{
55
+ responsibleInitials
56
+ }}</ResponsibleAvatar>
57
+ <ResponsibleTextStack>
58
+ <ResponsibleRoleLabel>{{
59
+ $gettext('responsible')
60
+ }}</ResponsibleRoleLabel>
61
+ <ResponsibleNameLine :title="responsibleDisplayName">{{
62
+ responsibleDisplayName
63
+ }}</ResponsibleNameLine>
64
+ </ResponsibleTextStack>
65
+ </ResponsibleSelectorRow>
66
+ </template>
67
+ <template #dropdown>
68
+ <SelectOption
69
+ v-for="(item, optionIdx) in projectManagerOptions"
70
+ :key="
71
+ item.id === null || item.id === undefined
72
+ ? `pm_opt_none_${optionIdx}`
73
+ : `pm_opt_${item.id}`
74
+ "
75
+ :data-id="`sidebar_responsible_option_${optionIdx + 1}`"
76
+ :data-qa-id="`sidebar_responsible_option_${
77
+ optionIdx + 1
78
+ }`"
79
+ min-width="min-content"
80
+ :value="item.id"
81
+ >
82
+ <ResponsibleOptionRow>
83
+ <ResponsibleOptionAvatar>{{
84
+ initialsFromFullName(item.full_name)
85
+ }}</ResponsibleOptionAvatar>
86
+ <SelectOptionText>{{
87
+ item.full_name
88
+ }}</SelectOptionText>
89
+ </ResponsibleOptionRow>
90
+ </SelectOption>
91
+ </template>
92
+ </SelectComponent>
93
+ </ResponsibleSelectSurface>
94
+ </ResponsibleCard>
95
+ </ResponsibleBlock>
96
+ <CollapseContainer
97
+ :is-collapsed="isCollapsed"
98
+ @click="toggleCollapse"
99
+ >
100
+ <CollapseButton
101
+ :aria-label="isCollapsed ? 'Expand menu' : 'Collapse menu'"
102
+ data-id="collapse_sidebar_button"
103
+ data-qa-id="collapse_sidebar_button"
104
+ >
105
+ <IconComponent
106
+ :name="!isCollapsed ? 'collapse_sidebar' : 'expand_sidebar'"
107
+ size="16px"
108
+ />
109
+ </CollapseButton>
110
+ </CollapseContainer>
111
+ </TitleContainer>
112
+ </TopMenuRow>
113
+ </HeaderSection>
114
+
115
+ <ScrollArea>
116
+ <ScrollContent ref="scrollContent" @scroll="updateScrollThumb">
117
+ <GroupsList>
118
+ <template v-for="(group, groupIdx) in menuData" :key="groupIdx">
119
+ <GroupSection
120
+ v-if="group.children?.length"
121
+ :is-collapsed="isCollapsed"
122
+ >
123
+ <GroupName v-if="!isCollapsed">{{ group.groupName }}</GroupName>
124
+ <ItemList>
125
+ <template
126
+ v-for="(item, itemIdx) in group.children"
127
+ :key="item.id ?? itemIdx"
128
+ >
129
+ <template v-if="!item.children?.length">
130
+ <MenuItem
131
+ v-if="item.type !== 'button'"
132
+ :data-id="item.dataId"
133
+ :data-qa-id="item.dataId"
134
+ :fill-type="item.fillType"
135
+ :is-active="item.id === activeItemId"
136
+ :is-collapsed="isCollapsed"
137
+ :is-disabled="item.isDisabled"
138
+ @click="
139
+ !item.isDisabled &&
140
+ $emit('on-menu-item-click', { item })
141
+ "
142
+ >
143
+ <IconCell :is-collapsed="isCollapsed">
144
+ <IconComponent
145
+ :color="
146
+ item.id === activeItemId
147
+ ? theme.semanticColors.purple[500]
148
+ : theme.semanticColors.teal[800]
149
+ "
150
+ :fill-type="item.fillType"
151
+ :hovered-color="theme.semanticColors.purple[500]"
152
+ :name="item.icon"
153
+ :size="item.iconSize || '16px'"
154
+ />
155
+ </IconCell>
156
+ <ItemLabel v-if="!isCollapsed">{{
157
+ item.name
158
+ }}</ItemLabel>
159
+ </MenuItem>
160
+ <MainButton
161
+ v-if="item.type === 'button' && !isCollapsed"
162
+ :custom-text-color="theme.semanticColors.purple[500]"
163
+ data-id="sub_menu_project_view_run_simulation"
164
+ data-qa-id="sub_menu_project_view_run_simulation"
165
+ :is-disabled="item.isDisabled"
166
+ no-background-color
167
+ :opacity-level="item.isDisabled ? '32%' : '100%'"
168
+ :text="item.name"
169
+ type="tertiary"
170
+ @click="
171
+ !item.isDisabled &&
172
+ $emit('on-menu-item-click', { item })
173
+ "
174
+ />
175
+ </template>
176
+ <CollapseWrapper v-else>
177
+ <MenuItem
178
+ :data-id="item.dataId"
179
+ :data-qa-id="item.dataId"
180
+ :fill-type="item.fillType"
181
+ :is-active="item.id === activeParentId"
182
+ :is-collapsed="isCollapsed"
183
+ :is-disabled="item.isDisabled"
184
+ :is-expandable="true"
185
+ :is-open="openDropdownId === item.id"
186
+ @click="!item.isDisabled && toggleDropdown(item.id)"
187
+ >
188
+ <IconCell :is-collapsed="isCollapsed">
189
+ <IconComponent
190
+ :color="
191
+ item.id === activeParentId
192
+ ? theme.semanticColors.purple[500]
193
+ : theme.semanticColors.teal[800]
194
+ "
195
+ :fill-type="item.fillType"
196
+ :hovered-color="theme.semanticColors.purple[500]"
197
+ :name="item.icon"
198
+ :size="item.iconSize || '16px'"
199
+ />
200
+ </IconCell>
201
+ <ItemLabel v-if="!isCollapsed">{{
202
+ item.name
203
+ }}</ItemLabel>
204
+ <ChevronCell
205
+ v-if="!isCollapsed"
206
+ :is-wrapper-hovered="isWrapperHovered"
207
+ >
208
+ <IconComponent
209
+ :color="
210
+ activeParentId === item.id
211
+ ? theme.semanticColors.purple[500]
212
+ : theme.semanticColors.teal[800]
213
+ "
214
+ :hovered-color="theme.semanticColors.purple[500]"
215
+ :name="
216
+ openDropdownId === item.id
217
+ ? 'chevron_up'
218
+ : 'chevron_down'
219
+ "
220
+ size="10px"
221
+ />
222
+ </ChevronCell>
223
+ </MenuItem>
224
+ <SubList
225
+ v-if="!isCollapsed && item.children?.length"
226
+ :is-visible="openDropdownId === item.id"
227
+ >
228
+ <SubItem
229
+ v-for="(sub, subIdx) in item.children"
230
+ :key="sub.id ?? subIdx"
231
+ :data-id="sub.dataId"
232
+ :data-qa-id="sub.dataId"
233
+ :is-active="sub.id === activeItemId"
234
+ :is-disabled="sub.isDisabled"
235
+ @click="
236
+ !sub.isDisabled &&
237
+ $emit('on-menu-item-click', { item: sub })
238
+ "
239
+ >
240
+ <span>{{ sub.name }}</span>
241
+ </SubItem>
242
+ </SubList>
243
+ </CollapseWrapper>
244
+ </template>
245
+ </ItemList>
246
+ </GroupSection>
247
+ </template>
248
+ </GroupsList>
249
+ </ScrollContent>
250
+ <ScrollbarTrack v-if="hasScrollOverflow" ref="scrollTrack">
251
+ <ScrollbarThumb
252
+ :style="scrollThumbStyle"
253
+ @mousedown.prevent="onThumbMouseDown"
254
+ />
255
+ </ScrollbarTrack>
256
+ </ScrollArea>
257
+ <FooterSection v-if="footerButtonText" :is-collapsed="isCollapsed">
258
+ <MainButton
259
+ v-if="!isCollapsed"
260
+ data-id="footer_documents_button"
261
+ data-qa-id="footer_documents_button"
262
+ :text="footerButtonText"
263
+ @click="onFooterButtonClick()"
264
+ />
265
+ <FooterButtonWrapper
266
+ v-else
267
+ data-id="footer_documents_button"
268
+ data-qa-id="footer_documents_button"
269
+ @click="onFooterButtonClick()"
270
+ >
271
+ <IconComponent color="white" name="documents" size="16px" />
272
+ </FooterButtonWrapper>
273
+ </FooterSection>
274
+ </SizerRef>
275
+ </PageWrapper>
276
+ </template>
277
+
278
+ <script>
279
+ import styled from 'vue3-styled-components'
280
+ import IconComponent from '../icon'
281
+ import theme from '@/assets/theme.js'
282
+ import MainButton from '../buttons/mainButton'
283
+ import SelectComponent from '../inputs/select'
284
+ import SelectOption from '../inputs/select/option'
285
+
286
+ /** Collapsed rail width; keep in sync with `emitSidebarDimensions` for layout consumers. */
287
+ const NAV_SIDEBAR_COLLAPSED_OUTER_WIDTH_PX = 48
288
+
289
+ const WrapperAttrs = { isCollapsed: Boolean, expandedWidth: Number }
290
+ const PageWrapper = styled('div', WrapperAttrs)`
291
+ display: flex;
292
+ flex-direction: column;
293
+ height: 100%;
294
+ user-select: none;
295
+ background-color: ${(p) => p.theme.semanticColors.grey[100]};
296
+ width: ${(p) =>
297
+ p.isCollapsed
298
+ ? `${NAV_SIDEBAR_COLLAPSED_OUTER_WIDTH_PX}px`
299
+ : p.expandedWidth
300
+ ? `${p.expandedWidth}px`
301
+ : 'max-content'};
302
+ min-width: ${(p) =>
303
+ p.isCollapsed ? `${NAV_SIDEBAR_COLLAPSED_OUTER_WIDTH_PX}px` : '0'};
304
+ border-right: 1px solid ${(p) => p.theme.semanticColors.grey[300]};
305
+ transition: width 0.25s ease;
306
+ overflow-x: hidden;
307
+ `
308
+
309
+ const SizerRef = styled.div`
310
+ display: flex;
311
+ flex-direction: column;
312
+ height: 100%;
313
+ width: max-content;
314
+ min-height: 0;
315
+ `
316
+
317
+ const HeaderSectionAttrs = { isCollapsed: Boolean }
318
+ const HeaderSection = styled('div', HeaderSectionAttrs)`
319
+ display: flex;
320
+ flex-direction: column;
321
+ justify-content: flex-start;
322
+ align-items: stretch;
323
+ gap: 12px;
324
+ padding: ${(p) => (p.isCollapsed ? '16px 10px' : '8px')};
325
+ border-bottom: 1px solid ${(p) => p.theme.semanticColors.grey[300]};
326
+ flex-shrink: 0;
327
+ `
328
+
329
+ const TopMenuRow = styled.div`
330
+ display: flex;
331
+ flex-direction: column;
332
+ width: 100%;
333
+ min-width: 0;
334
+ `
335
+
336
+ const TitleText = styled.div`
337
+ font-size: 15px;
338
+ font-weight: 600;
339
+ line-height: 150%;
340
+ letter-spacing: -0.01em;
341
+ color: ${(p) => p.theme.semanticColors.teal[800]};
342
+ white-space: nowrap;
343
+ overflow: hidden;
344
+ text-overflow: ellipsis;
345
+ min-width: 0;
346
+ flex: 1;
347
+ `
348
+
349
+ const CollapseButton = styled.div`
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ width: 32px;
354
+ height: 32px;
355
+ cursor: pointer;
356
+ flex-shrink: 0;
357
+ border: none;
358
+ border-radius: 8px;
359
+ background-color: none;
360
+ `
361
+
362
+ const ScrollArea = styled.div`
363
+ display: flex;
364
+ flex: 1;
365
+ min-width: 0;
366
+ min-height: 0;
367
+ `
368
+
369
+ const ScrollContent = styled.div`
370
+ flex: 1;
371
+ min-width: 0;
372
+ min-height: 0;
373
+ overflow-y: auto;
374
+ overflow-x: auto;
375
+ scrollbar-width: none;
376
+ -ms-overflow-style: none;
377
+
378
+ &::-webkit-scrollbar {
379
+ display: none;
380
+ }
381
+ `
382
+
383
+ const ScrollbarTrack = styled.div`
384
+ width: 4px;
385
+ flex-shrink: 0;
386
+ background: ${(p) => p.theme.semanticColors.grey[200]};
387
+ position: relative;
388
+ border-radius: 2px;
389
+ `
390
+
391
+ const ScrollbarThumb = styled.div`
392
+ position: absolute;
393
+ right: 100%;
394
+ margin-right: 2px;
395
+ width: 4px;
396
+ background-color: ${(p) => p.theme.semanticColors.grey[600]};
397
+ border-radius: 2px;
398
+ user-select: none;
399
+ `
400
+
401
+ const GroupsList = styled.div`
402
+ display: flex;
403
+ flex-direction: column;
404
+ `
405
+
406
+ const GroupSectionAttrs = { isCollapsed: Boolean }
407
+ const GroupSection = styled('div', GroupSectionAttrs)`
408
+ padding: ${(p) => (p.isCollapsed ? '10px 8px' : '10px 16px')};
409
+ `
410
+
411
+ const GroupName = styled.div`
412
+ font-size: 10px;
413
+ font-weight: 400;
414
+ color: ${(p) => p.theme.semanticColors.grey[700]};
415
+ text-transform: uppercase;
416
+ line-height: 150%;
417
+ `
418
+
419
+ const ItemList = styled.div`
420
+ display: flex;
421
+ flex-direction: column;
422
+ gap: 5px;
423
+ margin-top: 8px;
424
+ `
425
+
426
+ const MenuItemAttrs = {
427
+ fillType: String,
428
+ isDisabled: Boolean,
429
+ isCollapsed: Boolean,
430
+ isExpandable: Boolean,
431
+ isOpen: Boolean,
432
+ isActive: Boolean,
433
+ }
434
+ const MenuItem = styled('div', MenuItemAttrs)`
435
+ display: grid;
436
+ grid-template-columns: auto 1fr auto;
437
+ align-items: center;
438
+ gap: 8px;
439
+ border-radius: 8px;
440
+ padding: 4px;
441
+ min-width: max-content;
442
+ cursor: ${(p) => (p.isDisabled ? 'not-allowed' : 'pointer')};
443
+ opacity: ${(p) => (p.isDisabled ? 0.6 : 1)};
444
+ color: ${(p) => p.theme.semanticColors.teal[800]};
445
+ font-size: 14px;
446
+ font-weight: 400;
447
+ height: ${(p) => (p.isCollapsed ? '30px' : '32px')};
448
+ width: ${(p) => (p.isCollapsed ? '30px' : 'auto')};
449
+
450
+ ${(p) =>
451
+ p.isCollapsed &&
452
+ `
453
+ grid-template-columns: 1fr;
454
+ justify-items: center;
455
+ `}
456
+
457
+ ${(p) =>
458
+ p.isActive &&
459
+ `
460
+ background-color: ${p.theme.semanticColors.purple[100]};
461
+ color: ${p.theme.semanticColors.purple[500]};
462
+ `}
463
+
464
+ &:hover {
465
+ ${(p) =>
466
+ !p.isDisabled &&
467
+ `
468
+ background-color: ${p.theme.semanticColors.purple[50]};
469
+ color: ${p.theme.semanticColors.purple[500]};
470
+
471
+ svg path:not(.fix) {
472
+ ${p.fillType || 'fill'}: ${p.theme.semanticColors.purple[500]};
473
+ }
474
+
475
+ svg path.fix {
476
+ ${p.fillType || 'fill'}: ${p.theme.semanticColors.purple[800]};
477
+ }
478
+ `}
479
+ }
480
+ `
481
+
482
+ const IconCellAttrs = { isCollapsed: Boolean }
483
+ const IconCell = styled('div', IconCellAttrs)`
484
+ display: flex;
485
+ align-items: center;
486
+ justify-content: center;
487
+ flex-shrink: 0;
488
+ width: 24px;
489
+ ${(p) => p.isCollapsed && 'width: 100%;'}
490
+ `
491
+
492
+ const ItemLabel = styled.div`
493
+ white-space: nowrap;
494
+ font-size: 14px;
495
+ font-weight: 400;
496
+ `
497
+
498
+ const FooterButtonWrapper = styled.div`
499
+ display: flex;
500
+ align-items: center;
501
+ justify-content: center;
502
+ width: 30px;
503
+ height: 30px;
504
+ border-radius: 4px;
505
+ cursor: pointer;
506
+ background-color: ${(p) => p.theme.semanticColors.purple[500]};
507
+ `
508
+
509
+ const ChevronCellAttrs = { isWrapperHovered: Boolean }
510
+ const ChevronCell = styled('div', ChevronCellAttrs)`
511
+ display: flex;
512
+ align-items: center;
513
+ flex-shrink: 0;
514
+ visibility: ${(p) => (p.isWrapperHovered ? 'visible' : 'hidden')};
515
+ `
516
+
517
+ const CollapseWrapper = styled.div`
518
+ display: flex;
519
+ flex-direction: column;
520
+ `
521
+
522
+ const SubListAttrs = { isVisible: Boolean }
523
+ const SubList = styled('div', SubListAttrs)`
524
+ display: flex;
525
+ flex-direction: column;
526
+ margin-left: 48px;
527
+ margin-top: 5px;
528
+ gap: 5px;
529
+ min-width: max-content;
530
+ ${(p) =>
531
+ !p.isVisible &&
532
+ `
533
+ height: 0;
534
+ overflow: hidden;
535
+ visibility: hidden;
536
+ margin-top: 0;
537
+ margin-bottom: 0;
538
+ padding: 0;
539
+ gap: 0;
540
+ `}
541
+ `
542
+
543
+ const SubItemAttrs = { isDisabled: Boolean, isActive: Boolean }
544
+ const SubItem = styled('div', SubItemAttrs)`
545
+ display: grid;
546
+ grid-template-columns: auto 1fr;
547
+ align-items: center;
548
+ gap: 12px;
549
+ padding: 4px 8px;
550
+ min-width: max-content;
551
+ height: 32px;
552
+ border-radius: 8px;
553
+ cursor: ${(p) => (p.isDisabled ? 'not-allowed' : 'pointer')};
554
+ opacity: ${(p) => (p.isDisabled ? 0.6 : 1)};
555
+ color: ${(p) => p.theme.semanticColors.teal[800]};
556
+ font-size: 14px;
557
+
558
+ ${(p) =>
559
+ p.isActive &&
560
+ `
561
+ background-color: ${p.theme.semanticColors.purple[50]};
562
+ color: ${p.theme.semanticColors.purple[500]};
563
+ `}
564
+
565
+ &:hover {
566
+ ${(p) =>
567
+ !p.isDisabled &&
568
+ `
569
+ background-color: ${p.theme.semanticColors.purple[50]};
570
+ color: ${p.theme.semanticColors.purple[500]};
571
+ `}
572
+ }
573
+ `
574
+
575
+ const TitleContainerAttrs = { isCollapsed: Boolean }
576
+ const TitleContainer = styled('div', TitleContainerAttrs)`
577
+ display: flex;
578
+ justify-content: ${(p) => (p.isCollapsed ? 'flex-end' : 'space-between')};
579
+ align-items: center;
580
+ gap: 6px;
581
+ width: 100%;
582
+ min-width: 0;
583
+ min-height: 32px;
584
+ `
585
+
586
+ const FooterSectionAttrs = { isCollapsed: Boolean }
587
+ const FooterSection = styled('div', FooterSectionAttrs)`
588
+ padding: ${(p) => (p.isCollapsed ? '8px 10px' : '8px 16px')};
589
+ border-top: 1px solid ${(p) => p.theme.semanticColors.grey[300]};
590
+ `
591
+
592
+ const CollapseContainerAttrs = { isCollapsed: Boolean }
593
+ const CollapseContainer = styled('div', CollapseContainerAttrs)`
594
+ cursor: pointer;
595
+ display: flex;
596
+ align-items: center;
597
+ justify-content: center;
598
+ flex-shrink: 0;
599
+ margin-left: ${(p) => (p.isCollapsed ? '0' : '0')};
600
+ `
601
+
602
+ const ResponsibleBlock = styled.div`
603
+ display: flex;
604
+ flex: 1;
605
+ min-width: 0;
606
+ `
607
+
608
+ const ResponsibleCard = styled.div`
609
+ width: 100%;
610
+ min-width: 0;
611
+ box-sizing: border-box;
612
+ padding: 0;
613
+ border-radius: 8px;
614
+ overflow: hidden;
615
+ `
616
+
617
+ const ResponsibleSelectSurface = styled.div`
618
+ width: 100%;
619
+ min-width: 0;
620
+ border-radius: 8px;
621
+ background-color: transparent;
622
+ transition: background-color 0.12s ease;
623
+
624
+ &:hover {
625
+ background-color: ${(p) => p.theme.semanticColors.grey[300]};
626
+ }
627
+
628
+ &.ResponsibleSelectSurface--open {
629
+ background-color: ${(p) => p.theme.semanticColors.grey[300]};
630
+ }
631
+ `
632
+
633
+ const ResponsibleSelectorRow = styled.div`
634
+ display: flex;
635
+ flex-direction: row;
636
+ align-items: center;
637
+ gap: 10px;
638
+ width: 100%;
639
+ min-width: 0;
640
+ padding: 4px 4px 4px 2px;
641
+ `
642
+
643
+ const ResponsibleAvatar = styled.div`
644
+ flex-shrink: 0;
645
+ width: 24px;
646
+ height: 24px;
647
+ border-radius: 32px;
648
+ display: flex;
649
+ align-items: center;
650
+ justify-content: center;
651
+ overflow: hidden;
652
+ background-color: ${(p) => p.theme.semanticColors.grey[400]};
653
+ font-size: 10px;
654
+ font-weight: 500;
655
+ line-height: 1.4;
656
+ letter-spacing: -0.01em;
657
+ color: ${(p) => p.theme.semanticColors.grey[800]};
658
+ `
659
+
660
+ const ResponsibleTextStack = styled.div`
661
+ display: flex;
662
+ flex-direction: column;
663
+ align-items: flex-start;
664
+ justify-content: center;
665
+ gap: 2px;
666
+ min-width: 0;
667
+ flex: 1 1 auto;
668
+ `
669
+
670
+ const ResponsibleRoleLabel = styled.span`
671
+ font-size: 10px;
672
+ font-weight: 500;
673
+ color: ${(p) => p.theme.semanticColors.grey[700]};
674
+ `
675
+
676
+ const ResponsibleNameLine = styled.span`
677
+ font-size: 12px;
678
+ font-weight: 400;
679
+ color: ${(p) => p.theme.semanticColors.teal[800]};
680
+ max-width: 30ch;
681
+ white-space: nowrap;
682
+ overflow: hidden;
683
+ text-overflow: ellipsis;
684
+ `
685
+
686
+ const ResponsibleOptionRow = styled.div`
687
+ display: flex;
688
+ flex-direction: row;
689
+ align-items: center;
690
+ gap: 10px;
691
+ width: max-content;
692
+ min-width: 100%;
693
+ box-sizing: border-box;
694
+ `
695
+
696
+ const ResponsibleOptionAvatar = styled.div`
697
+ flex-shrink: 0;
698
+ width: 24px;
699
+ height: 24px;
700
+ border-radius: 32px;
701
+ display: flex;
702
+ align-items: center;
703
+ justify-content: center;
704
+ overflow: hidden;
705
+ background-color: ${(p) => p.theme.semanticColors.grey[300]};
706
+ font-size: 10px;
707
+ font-weight: 500;
708
+ color: ${(p) => p.theme.semanticColors.grey[800]};
709
+ `
710
+
711
+ const SelectOptionText = styled.span`
712
+ font-size: 14px;
713
+ font-weight: 400;
714
+ color: ${(p) => p.theme.semanticColors.grey[800]};
715
+ flex: 0 1 auto;
716
+ white-space: nowrap;
717
+ overflow: visible;
718
+ `
719
+
720
+ export default {
721
+ name: 'NavigationSideMenu',
722
+ components: {
723
+ PageWrapper,
724
+ SizerRef,
725
+ HeaderSection,
726
+ TopMenuRow,
727
+ TitleText,
728
+ CollapseButton,
729
+ ScrollArea,
730
+ ScrollContent,
731
+ ScrollbarTrack,
732
+ ScrollbarThumb,
733
+ GroupsList,
734
+ GroupSection,
735
+ GroupName,
736
+ ItemList,
737
+ MenuItem,
738
+ IconCell,
739
+ ItemLabel,
740
+ ChevronCell,
741
+ CollapseWrapper,
742
+ SubList,
743
+ SubItem,
744
+ IconComponent,
745
+ TitleContainer,
746
+ MainButton,
747
+ FooterSection,
748
+ FooterButtonWrapper,
749
+ CollapseContainer,
750
+ SelectComponent,
751
+ SelectOption,
752
+ ResponsibleBlock,
753
+ ResponsibleCard,
754
+ ResponsibleSelectSurface,
755
+ ResponsibleSelectorRow,
756
+ ResponsibleAvatar,
757
+ ResponsibleTextStack,
758
+ ResponsibleRoleLabel,
759
+ ResponsibleNameLine,
760
+ ResponsibleOptionRow,
761
+ ResponsibleOptionAvatar,
762
+ SelectOptionText,
763
+ },
764
+ props: {
765
+ menuData: {
766
+ type: Array,
767
+ default: () => [],
768
+ },
769
+ showProjectManagerSelect: {
770
+ type: Boolean,
771
+ default: false,
772
+ },
773
+ projectManagerOptions: {
774
+ type: Array,
775
+ default: () => [],
776
+ },
777
+ projectManagerId: {
778
+ type: [Number, String],
779
+ default: null,
780
+ },
781
+ projectManagerDisabled: {
782
+ type: Boolean,
783
+ default: false,
784
+ },
785
+ headerTitle: {
786
+ type: String,
787
+ default: '',
788
+ },
789
+ activeItemId: {
790
+ type: String,
791
+ default: null,
792
+ },
793
+ activeParentId: {
794
+ type: String,
795
+ default: null,
796
+ },
797
+ activeLanguage: {
798
+ type: String,
799
+ default: 'en',
800
+ },
801
+ footerButtonText: {
802
+ type: String,
803
+ default: '',
804
+ },
805
+ },
806
+ emits: [
807
+ 'item-click',
808
+ 'collapse-toggle',
809
+ 'project-manager-change',
810
+ 'on-menu-item-click',
811
+ 'on-footer-button-click',
812
+ 'sidebar-dimensions',
813
+ ],
814
+ data() {
815
+ return {
816
+ theme,
817
+ isCollapsed: false,
818
+ responsibleSelectSurfaceOpen: false,
819
+ openDropdownId: null,
820
+ isWrapperHovered: false,
821
+ expandedWidth: null,
822
+ scrollThumbStyle: {},
823
+ hasScrollOverflow: false,
824
+ thumbDragStartY: 0,
825
+ thumbDragStartScrollTop: 0,
826
+ titleRowResizeObserver: null,
827
+ sidebarLayoutResizeObserver: null,
828
+ }
829
+ },
830
+ computed: {
831
+ responsibleDisplayName() {
832
+ return this.getSelectValue(
833
+ this.projectManagerOptions,
834
+ this.projectManagerId,
835
+ 'full_name',
836
+ 'id'
837
+ )
838
+ },
839
+ responsibleInitials() {
840
+ return this.initialsFromFullName(this.responsibleDisplayName)
841
+ },
842
+ },
843
+ watch: {
844
+ activeParentId: {
845
+ immediate: true,
846
+ handler(id) {
847
+ if (id) this.openDropdownId = id
848
+ },
849
+ },
850
+ isCollapsed() {
851
+ this.$nextTick(() => {
852
+ this.updateExpandedWidth()
853
+ this.emitSidebarDimensions()
854
+ })
855
+ },
856
+ menuData: {
857
+ handler() {
858
+ this.$nextTick(() => {
859
+ this.updateExpandedWidth()
860
+ this.updateScrollThumb()
861
+ })
862
+ },
863
+ },
864
+ openDropdownId() {
865
+ this.$nextTick(this.updateScrollThumb)
866
+ },
867
+ responsibleDisplayName() {
868
+ this.$nextTick(this.updateExpandedWidth)
869
+ },
870
+ projectManagerId() {
871
+ this.$nextTick(this.updateExpandedWidth)
872
+ },
873
+ showProjectManagerSelect(val) {
874
+ this.$nextTick(() => {
875
+ if (val) {
876
+ this.setupTitleRowResizeObserver()
877
+ } else {
878
+ this.teardownTitleRowResizeObserver()
879
+ }
880
+ this.updateExpandedWidth()
881
+ })
882
+ },
883
+ projectManagerOptions: {
884
+ deep: true,
885
+ handler() {
886
+ this.$nextTick(this.updateExpandedWidth)
887
+ },
888
+ },
889
+ },
890
+ mounted() {
891
+ this.updateExpandedWidth()
892
+ this.$nextTick(() => {
893
+ this.updateScrollThumb()
894
+ this.observeScrollContent()
895
+ this.setupTitleRowResizeObserver()
896
+ this.setupSidebarLayoutResizeObserver()
897
+ this.emitSidebarDimensions()
898
+ })
899
+ },
900
+ beforeUnmount() {
901
+ this.teardownSidebarLayoutResizeObserver()
902
+ this.teardownTitleRowResizeObserver()
903
+ this.scrollResizeObserver?.disconnect()
904
+ document.removeEventListener('mousemove', this.onThumbMouseMove)
905
+ document.removeEventListener('mouseup', this.onThumbMouseUp)
906
+ },
907
+ methods: {
908
+ getSelectValue(options, currentSelectedValue, labelKey, valueKey = 'id') {
909
+ const selectedValue = options.find(
910
+ (option) => option[valueKey] == currentSelectedValue
911
+ )
912
+ return selectedValue ? selectedValue[labelKey] : '-'
913
+ },
914
+ initialsFromFullName(name) {
915
+ if (name == null || name === '' || name === '-') {
916
+ return '—'
917
+ }
918
+ const parts = String(name).trim().split(/\s+/).filter(Boolean)
919
+ if (!parts.length) {
920
+ return '?'
921
+ }
922
+ if (parts.length === 1) {
923
+ return parts[0].slice(0, 2).toUpperCase()
924
+ }
925
+ const a = parts[0][0] || ''
926
+ const b = parts[parts.length - 1][0] || ''
927
+ return (a + b).toUpperCase()
928
+ },
929
+ onResponsibleSelectDropdownOpen() {
930
+ this.responsibleSelectSurfaceOpen = true
931
+ },
932
+ onResponsibleSelectDropdownClose() {
933
+ this.responsibleSelectSurfaceOpen = false
934
+ },
935
+ onProjectManagerInputChange(value) {
936
+ this.$emit('project-manager-change', value)
937
+ },
938
+ updateExpandedWidth() {
939
+ if (this.isCollapsed) {
940
+ this.emitSidebarDimensions()
941
+ return
942
+ }
943
+ this.$nextTick(() => {
944
+ const el = this.$refs.sizerRef?.$el ?? this.$refs.sizerRef
945
+ if (!el) return
946
+ const titleRow = this.$refs.titleRowRef?.$el ?? this.$refs.titleRowRef
947
+ let w = el.offsetWidth
948
+ if (this.showProjectManagerSelect && titleRow) {
949
+ w = Math.max(w, titleRow.scrollWidth)
950
+ }
951
+ this.expandedWidth = w
952
+ this.emitSidebarDimensions()
953
+ })
954
+ },
955
+ /**
956
+ * Pushes outer width to parent (e.g. 3D ResizeHandle). Uses live layout read — call from
957
+ * ResizeObserver after width transition so expand-from-collapse updates correctly.
958
+ */
959
+ emitSidebarDimensionsImmediate() {
960
+ if (this.isCollapsed) {
961
+ this.$emit('sidebar-dimensions', {
962
+ widthPx: NAV_SIDEBAR_COLLAPSED_OUTER_WIDTH_PX,
963
+ })
964
+ return
965
+ }
966
+ const el = this.$el
967
+ const w = el?.offsetWidth
968
+ if (!w) return
969
+ this.$emit('sidebar-dimensions', { widthPx: w })
970
+ },
971
+ emitSidebarDimensions() {
972
+ this.$nextTick(() => this.emitSidebarDimensionsImmediate())
973
+ },
974
+ setupSidebarLayoutResizeObserver() {
975
+ this.teardownSidebarLayoutResizeObserver()
976
+ if (typeof ResizeObserver === 'undefined') return
977
+ const el = this.$el
978
+ if (!el) return
979
+ this.sidebarLayoutResizeObserver = new ResizeObserver(() => {
980
+ this.emitSidebarDimensionsImmediate()
981
+ })
982
+ this.sidebarLayoutResizeObserver.observe(el)
983
+ },
984
+ teardownSidebarLayoutResizeObserver() {
985
+ this.sidebarLayoutResizeObserver?.disconnect()
986
+ this.sidebarLayoutResizeObserver = null
987
+ },
988
+ setupTitleRowResizeObserver() {
989
+ this.teardownTitleRowResizeObserver()
990
+ if (
991
+ !this.showProjectManagerSelect ||
992
+ typeof ResizeObserver === 'undefined'
993
+ ) {
994
+ return
995
+ }
996
+ const row = this.$refs.titleRowRef?.$el ?? this.$refs.titleRowRef
997
+ if (!row) return
998
+ this.titleRowResizeObserver = new ResizeObserver(() => {
999
+ this.updateExpandedWidth()
1000
+ })
1001
+ this.titleRowResizeObserver.observe(row)
1002
+ },
1003
+ teardownTitleRowResizeObserver() {
1004
+ this.titleRowResizeObserver?.disconnect()
1005
+ this.titleRowResizeObserver = null
1006
+ },
1007
+ getScrollContentEl() {
1008
+ const ref = this.$refs.scrollContent
1009
+ return ref?.$el ?? ref
1010
+ },
1011
+ onFooterButtonClick() {
1012
+ this.$emit('on-footer-button-click')
1013
+ },
1014
+ updateScrollThumb() {
1015
+ const el = this.getScrollContentEl()
1016
+ if (!el) return
1017
+ const { scrollHeight, clientHeight, scrollTop } = el
1018
+ this.hasScrollOverflow = scrollHeight > clientHeight
1019
+ if (!this.hasScrollOverflow) {
1020
+ this.scrollThumbStyle = { height: '100%', top: '0' }
1021
+ return
1022
+ }
1023
+ const trackHeight = clientHeight
1024
+ const thumbHeight = Math.max(
1025
+ 20,
1026
+ (clientHeight / scrollHeight) * trackHeight
1027
+ )
1028
+ const maxScroll = scrollHeight - clientHeight
1029
+ const thumbTop =
1030
+ maxScroll > 0
1031
+ ? (scrollTop / maxScroll) * (trackHeight - thumbHeight)
1032
+ : 0
1033
+ this.scrollThumbStyle = {
1034
+ height: `${thumbHeight}px`,
1035
+ top: `${thumbTop}px`,
1036
+ }
1037
+ },
1038
+ observeScrollContent() {
1039
+ const el = this.getScrollContentEl()
1040
+ if (!el || typeof ResizeObserver === 'undefined') return
1041
+ this.scrollResizeObserver = new ResizeObserver(() => {
1042
+ this.updateScrollThumb()
1043
+ })
1044
+ this.scrollResizeObserver.observe(el)
1045
+ },
1046
+ onThumbMouseDown(e) {
1047
+ const el = this.getScrollContentEl()
1048
+ if (!el || el.scrollHeight <= el.clientHeight) return
1049
+ this.thumbDragStartY = e.clientY
1050
+ this.thumbDragStartScrollTop = el.scrollTop
1051
+ document.addEventListener('mousemove', this.onThumbMouseMove, {
1052
+ passive: false,
1053
+ })
1054
+ document.addEventListener('mouseup', this.onThumbMouseUp)
1055
+ },
1056
+ onThumbMouseMove(e) {
1057
+ e.preventDefault()
1058
+ const el = this.getScrollContentEl()
1059
+ if (!el) return
1060
+ const { scrollHeight, clientHeight } = el
1061
+ const maxScroll = scrollHeight - clientHeight
1062
+ if (maxScroll <= 0) return
1063
+ const thumbHeight = Math.max(
1064
+ 20,
1065
+ (clientHeight / scrollHeight) * clientHeight
1066
+ )
1067
+ const trackHeight = clientHeight
1068
+ const thumbTravel = trackHeight - thumbHeight
1069
+ if (thumbTravel <= 0) return
1070
+ const deltaY = e.clientY - this.thumbDragStartY
1071
+ const scrollDelta = (deltaY / thumbTravel) * maxScroll
1072
+ const newScrollTop = Math.max(
1073
+ 0,
1074
+ Math.min(maxScroll, this.thumbDragStartScrollTop + scrollDelta)
1075
+ )
1076
+ el.scrollTop = newScrollTop
1077
+ },
1078
+ onThumbMouseUp() {
1079
+ document.removeEventListener('mousemove', this.onThumbMouseMove)
1080
+ document.removeEventListener('mouseup', this.onThumbMouseUp)
1081
+ },
1082
+ toggleCollapse() {
1083
+ this.isCollapsed = !this.isCollapsed
1084
+ this.$emit('collapse-toggle', this.isCollapsed)
1085
+ this.emitSidebarDimensions()
1086
+ },
1087
+ toggleDropdown(id) {
1088
+ if (this.isCollapsed) {
1089
+ this.isCollapsed = false
1090
+ this.$emit('collapse-toggle', this.isCollapsed)
1091
+ this.openDropdownId = id
1092
+ } else {
1093
+ this.openDropdownId = this.openDropdownId === id ? null : id
1094
+ }
1095
+ },
1096
+ },
1097
+ }
1098
+ </script>