@eturnity/eturnity_reusable_components 8.7.5-EPIC-8593.2 → 8.7.5-EPIC-8593.4

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 (108) hide show
  1. package/package.json +1 -1
  2. package/src/assets/icons/collapse_arrow_icon_white.svg +1 -0
  3. package/src/assets/svgIcons/ac_power.svg +4 -0
  4. package/src/assets/svgIcons/arrow_long_left.svg +3 -0
  5. package/src/assets/svgIcons/arrow_long_right.svg +3 -0
  6. package/src/assets/svgIcons/chassis_ground_symbol.svg +27 -0
  7. package/src/assets/svgIcons/dc_power.svg +8 -0
  8. package/src/assets/svgIcons/double_arrow_long.svg +4 -0
  9. package/src/assets/svgIcons/ed_ac.svg +3 -0
  10. package/src/assets/svgIcons/ed_acgrid.svg +4 -0
  11. package/src/assets/svgIcons/ed_arrow_both.svg +7 -0
  12. package/src/assets/svgIcons/ed_arrow_left.svg +7 -0
  13. package/src/assets/svgIcons/ed_arrow_right.svg +7 -0
  14. package/src/assets/svgIcons/ed_battery.svg +10 -0
  15. package/src/assets/svgIcons/ed_batteryacinverter.svg +16 -0
  16. package/src/assets/svgIcons/ed_batteryintegratedinverter.svg +19 -0
  17. package/src/assets/svgIcons/ed_cirquitbreaker.svg +4 -0
  18. package/src/assets/svgIcons/ed_cirquitbreaker_magnetic.svg +6 -0
  19. package/src/assets/svgIcons/ed_cirquitbreaker_thermal.svg +4 -0
  20. package/src/assets/svgIcons/ed_cirquitbreaker_thermal_magnetic.svg +5 -0
  21. package/src/assets/svgIcons/ed_consumption.svg +3 -0
  22. package/src/assets/svgIcons/ed_dc.svg +6 -0
  23. package/src/assets/svgIcons/ed_disconnector.svg +4 -0
  24. package/src/assets/svgIcons/ed_disconnector_fuse.svg +4 -0
  25. package/src/assets/svgIcons/ed_disconnector_fuse_switch.svg +4 -0
  26. package/src/assets/svgIcons/ed_disconnector_loadbreak switch.svg +4 -0
  27. package/src/assets/svgIcons/ed_disconnector_switch.svg +4 -0
  28. package/src/assets/svgIcons/ed_disconnector_switch_auto_release.svg +5 -0
  29. package/src/assets/svgIcons/ed_energymanagement_rectangle.svg +3 -0
  30. package/src/assets/svgIcons/ed_evcharger.svg +19 -0
  31. package/src/assets/svgIcons/ed_flexiblecomponent_circle.svg +3 -0
  32. package/src/assets/svgIcons/ed_flexiblecomponent_square.svg +3 -0
  33. package/src/assets/svgIcons/ed_fuse.svg +3 -0
  34. package/src/assets/svgIcons/ed_ground.svg +5 -0
  35. package/src/assets/svgIcons/ed_heatpump.svg +4 -0
  36. package/src/assets/svgIcons/ed_icon_battery.svg +9 -0
  37. package/src/assets/svgIcons/ed_icon_circle.svg +3 -0
  38. package/src/assets/svgIcons/ed_icon_heatpump.svg +3 -0
  39. package/src/assets/svgIcons/ed_icon_inverter.svg +8 -0
  40. package/src/assets/svgIcons/ed_icon_optimizer.svg +11 -0
  41. package/src/assets/svgIcons/ed_integratedbatteryinverter.svg +10 -0
  42. package/src/assets/svgIcons/ed_inverter-blank.svg +3 -0
  43. package/src/assets/svgIcons/ed_mainsconnection.svg +3 -0
  44. package/src/assets/svgIcons/ed_meter_arrowleft.svg +4 -0
  45. package/src/assets/svgIcons/ed_meter_arrowright.svg +4 -0
  46. package/src/assets/svgIcons/ed_meter_bidirectional.svg +5 -0
  47. package/src/assets/svgIcons/ed_networkandsystemprotection_double.svg +14 -0
  48. package/src/assets/svgIcons/ed_networkandsystemprotection_single.svg +7 -0
  49. package/src/assets/svgIcons/ed_pvpanel.svg +7 -0
  50. package/src/assets/svgIcons/ed_rcd.svg +5 -0
  51. package/src/assets/svgIcons/ed_rcd_simple.svg +3 -0
  52. package/src/assets/svgIcons/ed_spd.svg +6 -0
  53. package/src/assets/svgIcons/ed_stringwithoptimizer.svg +33 -0
  54. package/src/assets/svgIcons/ed_stringwithoutoptimizer.svg +17 -0
  55. package/src/assets/svgIcons/ed_transformer.svg +3 -0
  56. package/src/assets/svgIcons/ground_symbol.svg +28 -0
  57. package/src/assets/svgIcons/house_sun.svg +3 -0
  58. package/src/assets/svgIcons/move_left.svg +3 -0
  59. package/src/assets/svgIcons/move_right.svg +3 -0
  60. package/src/assets/svgIcons/rectangle.svg +3 -0
  61. package/src/assets/svgIcons/refresh.svg +3 -0
  62. package/src/assets/svgIcons/text_icon.svg +3 -0
  63. package/src/assets/theme.js +18 -1
  64. package/src/components/banner/infoBanner/InfoBanner.spec.js +29 -42
  65. package/src/components/barchart/BottomFields.vue +253 -0
  66. package/src/components/barchart/ChartControls.vue +113 -0
  67. package/src/components/barchart/SelectionBox.vue +150 -0
  68. package/src/components/barchart/composables/index.js +5 -0
  69. package/src/components/barchart/composables/useAxisCalculations.js +104 -0
  70. package/src/components/barchart/composables/useChartData.js +114 -0
  71. package/src/components/barchart/composables/useChartScroll.js +61 -0
  72. package/src/components/barchart/composables/useSelection.js +75 -0
  73. package/src/components/barchart/composables/useTooltip.js +100 -0
  74. package/src/components/barchart/index.vue +385 -0
  75. package/src/components/barchart/styles/bottomFields.js +66 -0
  76. package/src/components/barchart/styles/chart.js +272 -0
  77. package/src/components/barchart/styles/chartControls.js +59 -0
  78. package/src/components/buttons/buttonIcon/index.vue +5 -0
  79. package/src/components/buttons/splitButtons/index.vue +86 -0
  80. package/src/components/collapsableInfoText/index.vue +2 -2
  81. package/src/components/draggableCard/defaultProps.js +16 -0
  82. package/src/components/draggableCard/draggableCard.spec.js +99 -0
  83. package/src/components/draggableCard/draggableCard.stories.js +79 -0
  84. package/src/components/draggableCard/index.vue +363 -0
  85. package/src/components/errorMessage/errorMessage.spec.js +34 -0
  86. package/src/components/errorMessage/errorMessage.stories.js +35 -0
  87. package/src/components/filter/filterSettings.vue +2 -0
  88. package/src/components/icon/index.vue +32 -9
  89. package/src/components/infoText/index.vue +2 -2
  90. package/src/components/infoText/infoText.spec.js +6 -1
  91. package/src/components/inputs/checkbox/index.vue +2 -2
  92. package/src/components/inputs/inputNumber/index.vue +14 -2
  93. package/src/components/inputs/searchInput/index.vue +18 -2
  94. package/src/components/inputs/select/index.vue +104 -13
  95. package/src/components/modals/actionModal/actionModal.spec.js +52 -0
  96. package/src/components/modals/actionModal/actionModal.stories.js +53 -0
  97. package/src/components/modals/actionModal/index.vue +6 -6
  98. package/src/components/modals/infoModal/index.vue +49 -19
  99. package/src/components/modals/infoModal/infoModal.spec.js +55 -0
  100. package/src/components/modals/infoModal/infoModal.stories.js +47 -0
  101. package/src/components/modals/modal/index.vue +16 -5
  102. package/src/components/pageSubtitle/PageSubtitle.stories.js +0 -1
  103. package/src/components/spinnerGif/index.vue +3 -3
  104. package/src/components/tabsHeader/index.vue +29 -1
  105. package/src/helpers/dateTimeFormatting.js +51 -0
  106. package/src/helpers/isObjectEqual.js +22 -0
  107. package/src/helpers/toLocaleNumber.js +11 -0
  108. package/src/main.js +1 -0
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M8 0.5C8.27614 0.5 8.5 0.723858 8.5 1V2.86667C8.5 3.14281 8.27614 3.36667 8 3.36667C7.72386 3.36667 7.5 3.14281 7.5 2.86667V1C7.5 0.723858 7.72386 0.5 8 0.5ZM0.5 8C0.5 7.72386 0.723858 7.5 1 7.5H2.86667C3.14281 7.5 3.36667 7.72386 3.36667 8C3.36667 8.27614 3.14281 8.5 2.86667 8.5H1C0.723858 8.5 0.5 8.27614 0.5 8ZM12.6333 8C12.6333 7.72386 12.8572 7.5 13.1333 7.5H15C15.2761 7.5 15.5 7.72386 15.5 8C15.5 8.27614 15.2761 8.5 15 8.5H13.1333C12.8572 8.5 12.6333 8.27614 12.6333 8ZM8.99987 10.8145C9.09475 11.0816 9.39061 11.2244 9.64095 11.0915C10.059 10.8696 10.4293 10.5644 10.728 10.1927C11.1343 9.6872 11.3922 9.0787 11.4728 8.43514C11.5535 7.79158 11.4537 7.13827 11.1847 6.54812C10.9156 5.95796 10.4879 5.45417 9.94915 5.09297C9.41045 4.73177 8.78197 4.52736 8.13385 4.50256C7.48573 4.47776 6.84347 4.63353 6.27873 4.9525C5.71399 5.27148 5.24899 5.7411 4.93563 6.30896C4.70526 6.72642 4.56349 7.1849 4.51696 7.65586C4.48909 7.93792 4.73355 8.15738 5.01665 8.14374C5.29975 8.1301 5.51274 7.8874 5.55771 7.60756C5.60274 7.32731 5.69588 7.05561 5.83426 6.80486C6.05573 6.40352 6.38437 6.07162 6.7835 5.84619C7.18262 5.62076 7.63654 5.51066 8.0946 5.52819C8.55265 5.54572 8.99683 5.69019 9.37756 5.94546C9.75829 6.20074 10.0606 6.55679 10.2507 6.97388C10.4409 7.39098 10.5114 7.8527 10.4544 8.30754C10.3974 8.76237 10.2152 9.19242 9.928 9.54971C9.74858 9.77293 9.5326 9.96227 9.29044 10.1103C9.04864 10.2582 8.90499 10.5474 8.99987 10.8145ZM5.17261 9.95833C5.36097 9.79137 5.64409 9.79046 5.83352 9.95621L8.82925 12.5775C8.93776 12.6724 9 12.8096 9 12.9538V14.5C9 14.7761 9.22386 15 9.5 15C9.77614 15 10 14.7761 10 14.5V12.9538C10 12.5212 9.81328 12.1097 9.48776 11.8249L6.49202 9.20363C5.92373 8.70638 5.0744 8.7091 4.5093 9.20998L1.50504 11.8728C1.18385 12.1575 1 12.5662 1 12.9954V14.5C1 14.7761 1.22386 15 1.5 15C1.77614 15 2 14.7761 2 14.5V12.9954C2 12.8523 2.06128 12.7161 2.16835 12.6212L5.17261 9.95833ZM3.40391 2.69668C3.20865 2.50142 2.89206 2.50142 2.6968 2.69668C2.50154 2.89194 2.50154 3.20852 2.6968 3.40379L4.01673 4.72372C4.212 4.91898 4.52858 4.91898 4.72384 4.72372C4.9191 4.52846 4.9191 4.21187 4.72384 4.01661L3.40391 2.69668ZM11.2764 4.01661C11.0811 4.21187 11.0811 4.52846 11.2764 4.72372C11.4716 4.91898 11.7882 4.91898 11.9835 4.72372L13.3034 3.40379C13.4987 3.20852 13.4987 2.89194 13.3034 2.69668C13.1081 2.50142 12.7916 2.50142 12.5963 2.69668L11.2764 4.01661ZM11.9835 11.2762C11.7882 11.081 11.4716 11.081 11.2764 11.2762C11.0811 11.4715 11.0811 11.7881 11.2764 11.9833L12.5963 13.3033C12.7916 13.4985 13.1081 13.4985 13.3034 13.3033C13.4987 13.108 13.4987 12.7914 13.3034 12.5962L11.9835 11.2762Z" fill="black"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0.875 6.36364H4.55V14H9.45V6.36364H13.125L7 0L0.875 6.36364Z" fill="#B2B9C5" transform="rotate(-90 7 7)"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0.875 6.36364H4.55V14H9.45V6.36364H13.125L7 0L0.875 6.36364Z" fill="#B2B9C5" transform="rotate(90 7 7)"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="2.5" y="2.5" width="11" height="11" rx="0.5" stroke="white"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path class="fix" d="M11.3335 12.5833C12.7477 11.5529 13.6668 9.88392 13.6668 8.00023C13.6668 4.87061 11.1298 2.33356 8.00016 2.33356H7.66683M8.00016 13.6669C4.87055 13.6669 2.3335 11.1298 2.3335 8.00023C2.3335 6.11654 3.25261 4.44751 4.66683 3.41716M7.3335 14.9336L8.66683 13.6002L7.3335 12.2669M8.66683 3.73356L7.3335 2.40023L8.66683 1.06689" stroke="#6F20DC" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 3C3.22386 3 3 3.22386 3 3.5V4.5C3 4.77614 2.77614 5 2.5 5C2.22386 5 2 4.77614 2 4.5V3.5C2 2.67157 2.67157 2 3.5 2H8H12.5C13.3284 2 14 2.67157 14 3.5V4.5C14 4.77614 13.7761 5 13.5 5C13.2239 5 13 4.77614 13 4.5V3.5C13 3.22386 12.7761 3 12.5 3H8.5V13H10.5C10.7761 13 11 13.2239 11 13.5C11 13.7761 10.7761 14 10.5 14H8H5.5C5.22386 14 5 13.7761 5 13.5C5 13.2239 5.22386 13 5.5 13H7.5V3H3.5Z" fill="white"/>
3
+ </svg>
@@ -71,6 +71,7 @@ const theme = (() => {
71
71
  transparentWhite2: '#ffffff32',
72
72
  transparentWhite1: '#ffffff16',
73
73
  transparentBlack1: '#263238e6',
74
+ transparentBlack2: '#263238e5',
74
75
  transparentBlue1: '#20a4cae6',
75
76
  blueElectric: '#66dffa',
76
77
  eturnityGrey: '#263238',
@@ -179,9 +180,25 @@ const theme = (() => {
179
180
  },
180
181
  }
181
182
 
183
+ const chartGradients = {
184
+ simple: {
185
+ from: semanticColors.purple[500],
186
+ to: semanticColors.purple[400],
187
+ },
188
+ stacked: [
189
+ { from: '#F5EDFF', to: '#DEC5FF' },
190
+ { from: '#CAA2FF', to: '#F2E8FF' },
191
+ { from: '#B987FC', to: '#904AEF' },
192
+ { from: '#8B40F2', to: '#4A1394' },
193
+ { from: '#6A05F2', to: '#4905A5' },
194
+ { from: '#5402C3', to: '#2B0362' },
195
+ ],
196
+ }
197
+
182
198
  return {
183
199
  colors,
184
200
  semanticColors,
201
+ chartGradients,
185
202
  fonts: {
186
203
  mainFont: '"Figtree", sans-serif',
187
204
  },
@@ -311,7 +328,7 @@ const theme = (() => {
311
328
  borderColor: semanticColors.grey[300],
312
329
  },
313
330
  hover: {
314
- backgroundColor: semanticColors.blue[200],
331
+ backgroundColor: semanticColors.purple[100],
315
332
  textColor: semanticColors.purple[700],
316
333
  borderColor: semanticColors.grey[300],
317
334
  },
@@ -1,5 +1,4 @@
1
1
  /* eslint-disable */
2
- import { h } from 'vue'
3
2
  import { mount } from '@vue/test-utils'
4
3
  import InfoBanner from '@/components/banner/infoBanner'
5
4
  import theme from '@/assets/theme'
@@ -10,59 +9,47 @@ jest.mock('@/components/icon/iconCache.mjs', () => ({
10
9
  }))
11
10
 
12
11
  describe('Info Banner Component', () => {
13
- const props = {
14
- isOpen: false,
15
- buttonLabel: 'Gotcha',
16
- }
17
-
18
- const global = {
19
- provide: {
20
- theme,
21
- },
22
- }
23
-
24
- it('info banner is shown when isOpen props is true', async () => {
25
- const wrapper = mount(InfoBanner, {
26
- props,
27
- global,
12
+ let wrapper
13
+
14
+ beforeEach(() => {
15
+ wrapper = mount(InfoBanner, {
16
+ props: {
17
+ isOpen: true,
18
+ buttonLabel: 'Gotcha',
19
+ },
20
+ slots: {
21
+ title: 'Sample title',
22
+ body: 'Sample body text',
23
+ },
24
+ global: {
25
+ provide: {
26
+ theme,
27
+ },
28
+ },
29
+ })
28
30
  })
29
31
 
32
+
33
+ it('info banner is shown when isOpen props is true', async () => {
34
+
30
35
  const bannerWrapper = wrapper.find('[data-test-id="info_banner_wrapper"]')
31
36
  expect(bannerWrapper.exists()).toBe(true)
32
- expect(bannerWrapper.classes()).not.toContain('visible')
33
- expect(bannerWrapper.classes()).toContain('hidden')
34
- await wrapper.setProps({ isOpen: true })
35
- expect(bannerWrapper.classes()).toContain('visible')
36
- expect(bannerWrapper.classes()).not.toContain('hidden')
37
37
  })
38
38
 
39
- it('info banner slots is display when user passed slots content', async () => {
40
- const titleText = 'Sample title'
41
- const bodyText = 'Sample body text'
42
-
43
- const wrapper = mount(InfoBanner, {
44
- props,
45
- slots: {
46
- title: titleText,
47
- body: bodyText,
48
- },
49
- global,
50
- })
51
-
39
+
40
+ it('info banner slots is display when user passed slots content', () => {
52
41
  const modalTitleEl = wrapper.find('[data-test-id="modal_title"]')
53
- expect(modalTitleEl.text()).toBe(titleText)
42
+ expect(modalTitleEl.text()).toBe('Sample title')
54
43
  const modalBodyEl = wrapper.find('[data-test-id="modal_body"]')
55
- expect(modalBodyEl.text()).toBe(bodyText)
44
+ expect(modalBodyEl.text()).toBe('Sample body text')
56
45
  })
57
46
 
58
47
  it('info banner on-close event is emitted when modal close button is clicked', async () => {
59
- const wrapper = mount(InfoBanner, {
60
- props,
61
- global,
62
- })
63
-
64
48
  const modalCloseButton = wrapper.find('.close')
65
- await modalCloseButton.trigger('click')
49
+
50
+ modalCloseButton.trigger('click')
51
+ await wrapper.vm.$nextTick()
52
+
66
53
  expect(wrapper.emitted('on-close')).toBeTruthy()
67
54
  const emittedEvent = wrapper.emitted('on-close')
68
55
  expect(emittedEvent).toHaveLength(1)
@@ -0,0 +1,253 @@
1
+ <template>
2
+ <Container :is-chart-controls-shown-in-bottom="isChartControlsShownInBottom">
3
+ <LabelsColumn :width="yAxisWidth">
4
+ <LabelRow v-for="series in props.series" :key="series.name">
5
+ {{ series.name }}
6
+ </LabelRow>
7
+ <TotalRow v-if="props.series.length && fieldMode === 'percentage'">
8
+ {{ $gettext ? $gettext('Total (%)') : 'Total (%)' }}
9
+ </TotalRow>
10
+ <TotalRow v-if="props.series.length">
11
+ {{ $gettext ? $gettext('Total (kWh)') : 'Total (kWh)' }}
12
+ </TotalRow>
13
+ </LabelsColumn>
14
+
15
+ <FieldsContainer
16
+ :class="`fields-container-${chartId}`"
17
+ @scroll="handleFieldsScroll"
18
+ >
19
+ <FieldsWrapper>
20
+ <!-- For stacked bar chart -->
21
+ <template v-if="props.series.length">
22
+ <InputRow
23
+ v-for="series in props.series"
24
+ :key="series.name"
25
+ :data-series-name="series.name"
26
+ >
27
+ <InputGroup
28
+ v-for="(item, index) in props.data"
29
+ :bar-width="barWidth"
30
+ :is-scrollable="isScrollable"
31
+ :key="index"
32
+ >
33
+ <InputNumber
34
+ :allow-negative="false"
35
+ input-height="36px"
36
+ :number-precision="0"
37
+ :min-decimals="0"
38
+ text-align="center"
39
+ :unit-name="fieldMode === 'percentage' ? '%' : ''"
40
+ :value="getDisplayValue(series.data, item.label)"
41
+ @input-blur="handleInputBlur($event, series.name, item.label)"
42
+ @input-focus="handleInputFocus(series.name, item.label)"
43
+ />
44
+ </InputGroup>
45
+ </InputRow>
46
+
47
+ <TotalInputRow v-if="fieldMode === 'percentage'">
48
+ <InputGroup
49
+ v-for="(item, index) in props.data"
50
+ :bar-width="barWidth"
51
+ :is-scrollable="isScrollable"
52
+ :key="index"
53
+ >
54
+ <InputNumber
55
+ :allow-negative="false"
56
+ input-height="36px"
57
+ :is-read-only="true"
58
+ :number-precision="0"
59
+ :min-decimals="0"
60
+ text-align="center"
61
+ :unit-name="fieldMode === 'percentage' ? '%' : ''"
62
+ :value="calculatePercentageTotal(item.label)"
63
+ />
64
+ </InputGroup>
65
+ </TotalInputRow>
66
+
67
+ <TotalInputRow>
68
+ <InputGroup
69
+ v-for="(item, index) in props.data"
70
+ :bar-width="barWidth"
71
+ :is-scrollable="isScrollable"
72
+ :key="index"
73
+ >
74
+ <InputNumber
75
+ input-height="36px"
76
+ :is-read-only="true"
77
+ :number-precision="2"
78
+ :min-decimals="0"
79
+ text-align="center"
80
+ :value="calculateTotal(item.label)"
81
+ />
82
+ </InputGroup>
83
+ </TotalInputRow>
84
+ </template>
85
+
86
+ <!-- For simple bar chart -->
87
+ <template v-else>
88
+ <InputRow>
89
+ <InputGroup
90
+ v-for="(item, index) in props.data"
91
+ :bar-width="barWidth"
92
+ :is-scrollable="isScrollable"
93
+ :key="index"
94
+ >
95
+ <InputNumber
96
+ input-height="36px"
97
+ :min-decimals="0"
98
+ :number-precision="2"
99
+ text-align="center"
100
+ :value="item.value"
101
+ @input-blur="handleInputBlur($event, null, item.label)"
102
+ @input-focus="handleInputFocus(null, item.label)"
103
+ />
104
+ </InputGroup>
105
+ </InputRow>
106
+ </template>
107
+ </FieldsWrapper>
108
+ </FieldsContainer>
109
+ </Container>
110
+ </template>
111
+
112
+ <script setup>
113
+ import { ref } from 'vue'
114
+ import InputNumber from '../inputs/inputNumber'
115
+
116
+ import {
117
+ Container,
118
+ LabelsColumn,
119
+ LabelRow,
120
+ TotalRow,
121
+ FieldsContainer,
122
+ FieldsWrapper,
123
+ InputRow,
124
+ TotalInputRow,
125
+ InputGroup,
126
+ } from './styles/bottomFields'
127
+
128
+ const props = defineProps({
129
+ chartId: {
130
+ type: String,
131
+ required: true,
132
+ },
133
+ data: {
134
+ type: Array,
135
+ required: true,
136
+ },
137
+ series: {
138
+ type: Array,
139
+ required: true,
140
+ },
141
+ barWidth: {
142
+ type: Number,
143
+ required: true,
144
+ },
145
+ yAxisWidth: {
146
+ type: String,
147
+ required: true,
148
+ },
149
+ isScrollable: {
150
+ type: Boolean,
151
+ default: false,
152
+ },
153
+ isChartControlsShownInBottom: {
154
+ type: Boolean,
155
+ default: false,
156
+ },
157
+ fieldMode: {
158
+ type: String,
159
+ default: 'absolute',
160
+ validator: (value) => ['absolute', 'percentage'].includes(value),
161
+ },
162
+ })
163
+
164
+ const emit = defineEmits([
165
+ 'sync-scroll',
166
+ 'input-blur',
167
+ 'input-focus',
168
+ 'input-blur-all',
169
+ ])
170
+
171
+ const focusedInput = ref(null)
172
+
173
+ const handleInputFocus = (seriesName, label) => {
174
+ focusedInput.value = { seriesName, label }
175
+ emit('input-focus', { seriesName, label })
176
+ }
177
+
178
+ const calculateTotal = (label) => {
179
+ return props.series.reduce((sum, series) => {
180
+ const value = series.data.find((d) => d.label === label)?.value || 0
181
+ return sum + value
182
+ }, 0)
183
+ }
184
+
185
+ const syncScroll = (scrollLeft) => {
186
+ const container = document.querySelector(
187
+ `.fields-container-${props.chartId}`
188
+ )
189
+ if (container) {
190
+ container.scrollLeft = scrollLeft
191
+ }
192
+ }
193
+ const getDisplayValue = (seriesData, label) => {
194
+ if (props.fieldMode === 'absolute') {
195
+ return seriesData.find((d) => d.label === label)?.value || ''
196
+ }
197
+
198
+ const value = seriesData.find((d) => d.label === label)?.value || 0
199
+ const total = calculateTotal(label)
200
+ return total ? Number(((value / total) * 100).toFixed(0)) : 0
201
+ }
202
+
203
+ const calculatePercentageTotal = (label) => {
204
+ return props.series.reduce((sum, series) => {
205
+ const value = series.data.find((d) => d.label === label)?.value || 0
206
+ const total = calculateTotal(label)
207
+ const percentage = total ? Number(((value / total) * 100).toFixed(0)) : 0
208
+ return sum + percentage
209
+ }, 0)
210
+ }
211
+
212
+ const handleInputBlur = (_value, seriesName, label) => {
213
+ let value = Number(_value)
214
+
215
+ if (props.fieldMode === 'percentage') {
216
+ const total = calculateTotal(label)
217
+ value = (value / 100) * total
218
+ }
219
+
220
+ const payload = seriesName ? { seriesName, label, value } : { label, value }
221
+ emit('input-blur', payload)
222
+ focusedInput.value = null
223
+
224
+ // Check if the related target (element receiving focus) is another input in our chart
225
+ const relatedTarget = document.activeElement
226
+ const currentChartContainer = document.querySelector(
227
+ `.fields-container-${props.chartId}`
228
+ )
229
+ const isMovingToAnotherInput =
230
+ currentChartContainer?.contains(relatedTarget) &&
231
+ relatedTarget?.classList.contains('input-number')
232
+
233
+ if (!isMovingToAnotherInput) {
234
+ emit('input-blur-all')
235
+ } else {
236
+ // If moving to another input, trigger focus event manually
237
+ const seriesName =
238
+ relatedTarget.closest('[data-series-name]')?.dataset.seriesName
239
+ const label = relatedTarget.closest('[data-label]')?.dataset.label
240
+ if (seriesName && label) {
241
+ handleInputFocus(seriesName, label)
242
+ }
243
+ }
244
+ }
245
+
246
+ const handleFieldsScroll = (event) => {
247
+ emit('sync-scroll', event.target.scrollLeft)
248
+ }
249
+
250
+ defineExpose({
251
+ syncScroll,
252
+ })
253
+ </script>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <Container>
3
+ <LegendAndControlsWrapper :leftMargin="yAxisWidth">
4
+ <LeftSection>
5
+ <SplitButtonsContainer
6
+ v-if="splitButtonOptions.length"
7
+ :position="position"
8
+ >
9
+ <SplitButtons
10
+ :modelValue="selectedSplitButton"
11
+ :options="splitButtonOptions"
12
+ @click="handleSplitButtonClick"
13
+ />
14
+ </SplitButtonsContainer>
15
+ </LeftSection>
16
+
17
+ <LegendGroups v-if="isLegendShown">
18
+ <LegendRow
19
+ v-for="(row, rowIndex) in groupedSeries"
20
+ :key="rowIndex"
21
+ :rowIndex="rowIndex"
22
+ >
23
+ <LegendItem v-for="(series, index) in row" :key="series.name">
24
+ <LegendColor
25
+ :gradientFrom="
26
+ reversedColors[rowIndex * legendsItemPerRow + index].from
27
+ "
28
+ :gradientTo="
29
+ reversedColors[rowIndex * legendsItemPerRow + index].to
30
+ "
31
+ />
32
+ <LegendText>{{ series.name }}</LegendText>
33
+ </LegendItem>
34
+ </LegendRow>
35
+ </LegendGroups>
36
+ </LegendAndControlsWrapper>
37
+ </Container>
38
+ </template>
39
+
40
+ <script setup>
41
+ import { computed } from 'vue'
42
+
43
+ import SplitButtons from '../buttons/splitButtons'
44
+
45
+ import {
46
+ Container,
47
+ LegendAndControlsWrapper,
48
+ LeftSection,
49
+ SplitButtonsContainer,
50
+ LegendGroups,
51
+ LegendRow,
52
+ LegendItem,
53
+ LegendColor,
54
+ LegendText,
55
+ } from './styles/chartControls'
56
+
57
+ const props = defineProps({
58
+ position: {
59
+ type: String,
60
+ required: true,
61
+ },
62
+ yAxisWidth: {
63
+ type: String,
64
+ required: true,
65
+ },
66
+ series: {
67
+ type: Array,
68
+ default: () => [],
69
+ },
70
+ stackedColors: {
71
+ type: Function,
72
+ required: true,
73
+ },
74
+ splitButtonOptions: {
75
+ type: Array,
76
+ default: () => [],
77
+ },
78
+ selectedSplitButton: {
79
+ type: String,
80
+ default: '',
81
+ },
82
+ isLegendShown: {
83
+ type: Boolean,
84
+ default: false,
85
+ },
86
+ legendsItemPerRow: {
87
+ type: Number,
88
+ required: true,
89
+ },
90
+ })
91
+
92
+ const emit = defineEmits(['select-split-button'])
93
+
94
+ const reversedColors = computed(() =>
95
+ [...props.stackedColors(props.series.length)].reverse()
96
+ )
97
+
98
+ const groupedSeries = computed(() => {
99
+ const { series, legendsItemPerRow } = props
100
+
101
+ return series.reduce((groups, item, index) => {
102
+ const groupIndex = Math.floor(index / legendsItemPerRow)
103
+ groups[groupIndex] = groups[groupIndex] || []
104
+ groups[groupIndex].push(item)
105
+
106
+ return groups
107
+ }, [])
108
+ })
109
+
110
+ const handleSplitButtonClick = (value) => {
111
+ emit('select-split-button', value)
112
+ }
113
+ </script>
@@ -0,0 +1,150 @@
1
+ <template>
2
+ <SelectionBoxWrapper
3
+ :transform="`translateX(${position}px)`"
4
+ :width="boxWidth + 'px'"
5
+ @mousedown="startDrag"
6
+ />
7
+ </template>
8
+
9
+ <script setup>
10
+ import { ref, computed, onMounted } from 'vue'
11
+ import styled from 'vue3-styled-components'
12
+
13
+ const SelectionBoxWrapper = styled('div', {
14
+ width: String,
15
+ transform: String,
16
+ })`
17
+ position: absolute;
18
+ height: 100%;
19
+ background: rgba(151, 71, 255, 0.1);
20
+ cursor: grab;
21
+ z-index: 0;
22
+ pointer-events: auto;
23
+ width: ${(props) => props.width};
24
+ transform: ${(props) => props.transform};
25
+ &:active {
26
+ cursor: grabbing;
27
+ }
28
+ `
29
+
30
+ const props = defineProps({
31
+ barsToShow: {
32
+ type: Number,
33
+ required: true,
34
+ },
35
+ barWidth: {
36
+ type: Number,
37
+ required: true,
38
+ },
39
+ totalBars: {
40
+ type: Number,
41
+ required: true,
42
+ },
43
+ gap: {
44
+ type: Number,
45
+ default: 8,
46
+ },
47
+ containerWidth: {
48
+ type: Number,
49
+ required: true,
50
+ },
51
+ isScrollable: {
52
+ type: Boolean,
53
+ required: true,
54
+ },
55
+ })
56
+
57
+ // Padding applied to the selection box
58
+ const PADDING = 6
59
+
60
+ const emit = defineEmits(['update-selection', 'drag-end'])
61
+
62
+ // Statess
63
+ const position = ref(0)
64
+ const isDragging = ref(false)
65
+ const currentSelection = ref({ startIndex: 0, endIndex: props.barsToShow })
66
+
67
+ // Variables for tracking drag state
68
+ let startX = 0
69
+ let startPos = 0
70
+
71
+ const actualBarWidth = computed(() => {
72
+ if (props.isScrollable) {
73
+ return props.barWidth
74
+ }
75
+
76
+ return props.containerWidth / props.totalBars
77
+ })
78
+
79
+ const boxWidth = computed(() => {
80
+ if (props.isScrollable) {
81
+ return (
82
+ (props.barWidth + props.gap) * props.barsToShow -
83
+ props.gap +
84
+ PADDING * 2
85
+ )
86
+ }
87
+
88
+ return actualBarWidth.value * props.barsToShow
89
+ })
90
+
91
+ const calculateSelection = () => {
92
+ if (props.isScrollable) {
93
+ const startIndex = Math.floor(
94
+ (position.value + PADDING) / (props.barWidth + props.gap)
95
+ )
96
+ const endIndex = Math.min(startIndex + props.barsToShow, props.totalBars)
97
+ return { startIndex, endIndex }
98
+ }
99
+
100
+ const boxStart = position.value
101
+ const boxEnd = position.value + boxWidth.value
102
+ const sectionWidth = props.containerWidth / props.totalBars
103
+
104
+ const startIndex = Math.round(boxStart / sectionWidth)
105
+ const endIndex = Math.min(
106
+ Math.round(boxEnd / sectionWidth),
107
+ props.totalBars
108
+ )
109
+ return { startIndex, endIndex }
110
+ }
111
+
112
+ const startDrag = (e) => {
113
+ isDragging.value = true
114
+ startX = e.clientX
115
+ startPos = position.value
116
+
117
+ document.addEventListener('mousemove', handleDrag)
118
+ document.addEventListener('mouseup', stopDrag)
119
+ }
120
+
121
+ const handleDrag = (e) => {
122
+ if (!isDragging.value) return
123
+
124
+ const delta = e.clientX - startX
125
+ const newPosition = startPos + delta
126
+
127
+ // Constrain position within valid bounds
128
+ const maxPosition = props.containerWidth - boxWidth.value + PADDING * 2
129
+ position.value = Math.max(-PADDING, Math.min(newPosition, maxPosition))
130
+
131
+ // Update current selection and emit event
132
+ currentSelection.value = calculateSelection()
133
+ emit('update-selection', currentSelection.value)
134
+ }
135
+
136
+ const stopDrag = () => {
137
+ isDragging.value = false
138
+ document.removeEventListener('mousemove', handleDrag)
139
+ document.removeEventListener('mouseup', stopDrag)
140
+
141
+ emit('update-selection', currentSelection.value)
142
+ emit('drag-end')
143
+ }
144
+
145
+ onMounted(() => {
146
+ // Ensure we start with valid selection indices
147
+ currentSelection.value = { startIndex: 0, endIndex: props.barsToShow }
148
+ emit('update-selection', currentSelection.value)
149
+ })
150
+ </script>
@@ -0,0 +1,5 @@
1
+ export { useTooltip } from './useTooltip'
2
+ export { useChartData } from './useChartData'
3
+ export { useAxisCalculations } from './useAxisCalculations'
4
+ export { useSelection } from './useSelection'
5
+ export { useChartScroll } from './useChartScroll'