@rancher/shell 3.0.9-rc.6 → 3.0.10

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 (82) hide show
  1. package/assets/styles/base/_color.scss +4 -0
  2. package/assets/styles/themes/_light.scss +6 -6
  3. package/assets/styles/themes/_modern.scss +14 -6
  4. package/assets/translations/en-us.yaml +2 -5
  5. package/components/CopyToClipboard.vue +28 -0
  6. package/components/CopyToClipboardText.vue +4 -0
  7. package/components/CruResource.vue +1 -0
  8. package/components/GlobalRoleBindings.vue +1 -5
  9. package/components/IconOrSvg.vue +61 -42
  10. package/components/ResourceDetail/index.vue +0 -21
  11. package/components/SortableTable/index.vue +2 -2
  12. package/components/__tests__/CruResource.test.ts +35 -1
  13. package/components/form/BannerSettings.vue +2 -2
  14. package/components/form/NotificationSettings.vue +2 -2
  15. package/composables/useIsNewDetailPageEnabled.test.ts +98 -0
  16. package/composables/useIsNewDetailPageEnabled.ts +12 -0
  17. package/config/product/explorer.js +11 -1
  18. package/config/product/manager.js +0 -1
  19. package/config/table-headers.js +0 -9
  20. package/config/types.js +0 -1
  21. package/detail/fleet.cattle.io.cluster.vue +1 -1
  22. package/dialog/FeatureFlagListDialog.vue +1 -1
  23. package/edit/auth/github-app-steps.vue +2 -0
  24. package/edit/auth/github-steps.vue +2 -0
  25. package/edit/catalog.cattle.io.clusterrepo.vue +1 -1
  26. package/edit/management.cattle.io.user.vue +60 -35
  27. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/auth.spec.ts +145 -0
  28. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/index.test.ts +202 -0
  29. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/tls.spec.ts +226 -0
  30. package/edit/monitoring.coreos.com.alertmanagerconfig/auth.vue +24 -21
  31. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/opsgenie.spec.ts +157 -0
  32. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/pagerduty.spec.ts +132 -0
  33. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/slack.spec.ts +108 -0
  34. package/edit/monitoring.coreos.com.alertmanagerconfig/types/pagerduty.vue +2 -1
  35. package/edit/monitoring.coreos.com.receiver/__tests__/auth.spec.ts +165 -0
  36. package/edit/monitoring.coreos.com.receiver/__tests__/index.test.ts +153 -0
  37. package/edit/monitoring.coreos.com.receiver/__tests__/tls.spec.ts +115 -0
  38. package/edit/monitoring.coreos.com.receiver/types/__tests__/email.spec.ts +86 -0
  39. package/edit/monitoring.coreos.com.receiver/types/__tests__/opsgenie.spec.ts +209 -0
  40. package/edit/monitoring.coreos.com.receiver/types/__tests__/pagerduty.spec.ts +105 -0
  41. package/edit/monitoring.coreos.com.receiver/types/__tests__/slack.spec.ts +92 -0
  42. package/edit/monitoring.coreos.com.receiver/types/__tests__/webhook.spec.ts +131 -0
  43. package/edit/provisioning.cattle.io.cluster/ingress/IngressCards.vue +14 -12
  44. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -5
  45. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +18 -3
  46. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +100 -76
  47. package/edit/token.vue +29 -68
  48. package/list/provisioning.cattle.io.cluster.vue +2 -2
  49. package/models/__tests__/chart.test.ts +2 -2
  50. package/models/chart.js +3 -3
  51. package/models/token.js +0 -4
  52. package/package.json +8 -8
  53. package/pages/account/index.vue +67 -96
  54. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +108 -24
  55. package/pages/c/_cluster/apps/charts/index.vue +1 -11
  56. package/pages/c/_cluster/explorer/index.vue +2 -19
  57. package/pages/c/_cluster/explorer/tools/index.vue +1 -1
  58. package/pages/c/_cluster/manager/cloudCredential/index.vue +1 -1
  59. package/pages/c/_cluster/uiplugins/index.vue +1 -1
  60. package/pkg/auto-import.js +41 -0
  61. package/plugins/dashboard-store/resource-class.js +2 -2
  62. package/plugins/steve/__tests__/steve-class.test.ts +1 -1
  63. package/plugins/steve/steve-class.js +3 -3
  64. package/plugins/steve/steve-pagination-utils.ts +2 -4
  65. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +7 -7
  66. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +5 -2
  67. package/rancher-components/RcIcon/types.ts +2 -2
  68. package/rancher-components/RcItemCard/RcItemCard.vue +8 -1
  69. package/rancher-components/RcSection/RcSection.test.ts +323 -0
  70. package/rancher-components/RcSection/RcSection.vue +252 -0
  71. package/rancher-components/RcSection/RcSectionActions.test.ts +212 -0
  72. package/rancher-components/RcSection/RcSectionActions.vue +85 -0
  73. package/rancher-components/RcSection/RcSectionBadges.test.ts +149 -0
  74. package/rancher-components/RcSection/RcSectionBadges.vue +29 -0
  75. package/rancher-components/RcSection/index.ts +12 -0
  76. package/rancher-components/RcSection/types.ts +86 -0
  77. package/scripts/test-plugins-build.sh +5 -4
  78. package/types/shell/index.d.ts +93 -108
  79. package/utils/style.ts +17 -0
  80. package/utils/svg-filter.js +4 -3
  81. package/utils/units.js +14 -5
  82. package/models/ext.cattle.io.token.js +0 -48
@@ -11,27 +11,27 @@ const displayCount = computed(() => props.count < 1000 ? props.count : '999+');
11
11
  :class="{[props.type]: true, disabled: props.disabled}"
12
12
  data-testid="rc-counter-badge"
13
13
  >
14
- {{ displayCount }}
14
+ <span class="count">{{ displayCount }}</span>
15
15
  </div>
16
16
  </template>
17
17
 
18
18
  <style lang="scss" scoped>
19
19
  .rc-counter-badge {
20
+ box-sizing: border-box;
21
+ height: 21px;
22
+
20
23
  display: inline-flex;
21
- padding: 1px 8px;
24
+ padding: 2px 8px;
22
25
  align-items: center;
23
- gap: 8px;
24
26
 
25
27
  border-radius: 30px;
26
28
  border: 1px solid var(--rc-active-border);
27
29
 
28
- overflow: hidden;
29
- text-overflow: ellipsis;
30
30
  font-family: Lato;
31
- font-size: 13px;
31
+ font-size: 12px;
32
32
  font-style: normal;
33
33
  font-weight: 400;
34
- line-height: 22px;
34
+ line-height: 17px;
35
35
  color: var(--body-text);
36
36
 
37
37
  &.active {
@@ -20,17 +20,20 @@ const { backgroundColor, borderColor, textColor } = useStatusColors(status, 'out
20
20
 
21
21
  <style lang="scss" scoped>
22
22
  .rc-status-badge {
23
+ box-sizing: border-box;
24
+ height: 21px;
25
+
23
26
  display: inline-flex;
24
27
  align-items: center;
25
28
  justify-content: center;
26
- padding: 1px 7px;
29
+ padding: 2px 7px;
27
30
 
28
31
  border: 1px solid transparent;
29
32
  border-radius: 30px;
30
33
 
31
34
  font-family: Lato;
32
35
  font-size: 12px;
33
- line-height: 19px;
36
+ line-height: 17px;
34
37
 
35
38
  background-color: v-bind(backgroundColor);
36
39
  border-color: v-bind(borderColor);
@@ -148,8 +148,8 @@ export const RcIconTypeToClass = {
148
148
  export const RcIconSizeToCSS = {
149
149
  xxlarge: '40px',
150
150
  xlarge: '32px',
151
- large: '25px',
152
- medium: '18px',
151
+ large: '24px',
152
+ medium: '16px',
153
153
  small: '14px',
154
154
  inherit: 'inherit'
155
155
  };
@@ -314,7 +314,10 @@ const cursorValue = computed(() => props.clickable ? 'pointer' : 'auto');
314
314
  </div>
315
315
  </div>
316
316
 
317
- <slot name="item-card-sub-header" />
317
+ <slot name="item-card-sub-header">
318
+ <!-- DIV added to add the gap if the sub-header is not provided -->
319
+ <div />
320
+ </slot>
318
321
 
319
322
  <template v-if="$slots['item-card-content']">
320
323
  <slot name="item-card-content">
@@ -407,6 +410,10 @@ $image-medium-box-width: 48px;
407
410
  height: 24px;
408
411
  color: var(--body-text);
409
412
 
413
+ &.small {
414
+ height: 32px;
415
+ }
416
+
410
417
  &-left,
411
418
  &-right {
412
419
  display: flex;
@@ -0,0 +1,323 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import RcSection from './RcSection.vue';
3
+
4
+ describe('component: RcSection', () => {
5
+ const defaultProps = {
6
+ type: 'primary' as const,
7
+ mode: 'with-header' as const,
8
+ background: 'primary' as const,
9
+ expandable: false,
10
+ title: 'Test title',
11
+ };
12
+
13
+ describe('type prop', () => {
14
+ it('should apply type-primary class when type is "primary"', () => {
15
+ const wrapper = mount(RcSection, { props: { ...defaultProps, type: 'primary' } });
16
+
17
+ expect(wrapper.find('.rc-section').classes()).toContain('type-primary');
18
+ });
19
+
20
+ it('should apply type-secondary class when type is "secondary"', () => {
21
+ const wrapper = mount(RcSection, { props: { ...defaultProps, type: 'secondary' } });
22
+
23
+ expect(wrapper.find('.rc-section').classes()).toContain('type-secondary');
24
+ });
25
+ });
26
+
27
+ describe('background prop', () => {
28
+ it('should apply bg-primary class when background is "primary"', () => {
29
+ const wrapper = mount(RcSection, { props: { ...defaultProps, background: 'primary' } });
30
+
31
+ expect(wrapper.find('.rc-section').classes()).toContain('bg-primary');
32
+ });
33
+
34
+ it('should apply bg-secondary class when background is "secondary"', () => {
35
+ const wrapper = mount(RcSection, { props: { ...defaultProps, background: 'secondary' } });
36
+
37
+ expect(wrapper.find('.rc-section').classes()).toContain('bg-secondary');
38
+ });
39
+
40
+ it('should default to "primary" background when no background prop and no parent', () => {
41
+ const { background: _, ...propsWithoutBg } = defaultProps;
42
+ const wrapper = mount(RcSection, { props: propsWithoutBg });
43
+
44
+ expect(wrapper.find('.rc-section').classes()).toContain('bg-primary');
45
+ });
46
+
47
+ it('should alternate background from parent via provide/inject', () => {
48
+ const wrapper = mount(RcSection, {
49
+ props: {
50
+ ...defaultProps, background: 'primary', expanded: true
51
+ },
52
+ slots: {
53
+ default: {
54
+ components: { RcSection },
55
+ template: '<RcSection type="secondary" mode="with-header" :expandable="false" title="Child" />',
56
+ },
57
+ },
58
+ });
59
+
60
+ const childSection = wrapper.findAll('.rc-section')[1];
61
+
62
+ expect(childSection.classes()).toContain('bg-secondary');
63
+ });
64
+
65
+ it('should allow explicit background to override the injected alternation', () => {
66
+ const wrapper = mount(RcSection, {
67
+ props: {
68
+ ...defaultProps, background: 'primary', expanded: true
69
+ },
70
+ slots: {
71
+ default: {
72
+ components: { RcSection },
73
+ template: '<RcSection type="secondary" mode="with-header" :expandable="false" background="primary" title="Child" />',
74
+ },
75
+ },
76
+ });
77
+
78
+ const childSection = wrapper.findAll('.rc-section')[1];
79
+
80
+ expect(childSection.classes()).toContain('bg-primary');
81
+ });
82
+ });
83
+
84
+ describe('mode prop', () => {
85
+ it('should render section-header when mode is "with-header"', () => {
86
+ const wrapper = mount(RcSection, { props: { ...defaultProps, mode: 'with-header' } });
87
+
88
+ expect(wrapper.find('.section-header').exists()).toBe(true);
89
+ });
90
+
91
+ it('should not render section-header when mode is "no-header"', () => {
92
+ const wrapper = mount(RcSection, { props: { ...defaultProps, mode: 'no-header' } });
93
+
94
+ expect(wrapper.find('.section-header').exists()).toBe(false);
95
+ });
96
+
97
+ it('should apply no-header class to content when mode is "no-header"', () => {
98
+ const wrapper = mount(RcSection, { props: { ...defaultProps, mode: 'no-header' } });
99
+
100
+ expect(wrapper.find('.section-content').classes()).toContain('no-header');
101
+ });
102
+ });
103
+
104
+ describe('title prop', () => {
105
+ it('should render the title text', () => {
106
+ const wrapper = mount(RcSection, { props: { ...defaultProps, title: 'My Section' } });
107
+
108
+ expect(wrapper.find('.title').text()).toBe('My Section');
109
+ });
110
+
111
+ it('should render the title slot when provided', () => {
112
+ const wrapper = mount(RcSection, {
113
+ props: { ...defaultProps },
114
+ slots: { title: '<span class="custom-title">Custom</span>' },
115
+ });
116
+
117
+ expect(wrapper.find('.custom-title').exists()).toBe(true);
118
+ expect(wrapper.find('.custom-title').text()).toBe('Custom');
119
+ });
120
+ });
121
+
122
+ describe('expandable behavior', () => {
123
+ it('should render toggle button when expandable is true', () => {
124
+ const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: true } });
125
+
126
+ expect(wrapper.find('.toggle-button').exists()).toBe(true);
127
+ });
128
+
129
+ it('should not render toggle button when expandable is false', () => {
130
+ const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: false } });
131
+
132
+ expect(wrapper.find('.toggle-button').exists()).toBe(false);
133
+ });
134
+
135
+ it('should set aria-expanded on toggle button when expandable', () => {
136
+ const wrapper = mount(RcSection, {
137
+ props: {
138
+ ...defaultProps, expandable: true, expanded: true
139
+ }
140
+ });
141
+
142
+ expect(wrapper.find('.toggle-button').attributes('aria-expanded')).toBe('true');
143
+ });
144
+
145
+ it('should set aria-expanded="false" on toggle button when collapsed', () => {
146
+ const wrapper = mount(RcSection, {
147
+ props: {
148
+ ...defaultProps, expandable: true, expanded: false
149
+ }
150
+ });
151
+
152
+ expect(wrapper.find('.toggle-button').attributes('aria-expanded')).toBe('false');
153
+ });
154
+
155
+ it('should set aria-label to "Collapse section" on toggle button when expanded', () => {
156
+ const wrapper = mount(RcSection, {
157
+ props: {
158
+ ...defaultProps, expandable: true, expanded: true
159
+ }
160
+ });
161
+
162
+ expect(wrapper.find('.toggle-button').attributes('aria-label')).toBe('Collapse section');
163
+ });
164
+
165
+ it('should set aria-label to "Expand section" on toggle button when collapsed', () => {
166
+ const wrapper = mount(RcSection, {
167
+ props: {
168
+ ...defaultProps, expandable: true, expanded: false
169
+ }
170
+ });
171
+
172
+ expect(wrapper.find('.toggle-button').attributes('aria-label')).toBe('Expand section');
173
+ });
174
+
175
+ it('should emit update:expanded with false when clicking an expanded header', async() => {
176
+ const wrapper = mount(RcSection, {
177
+ props: {
178
+ ...defaultProps, expandable: true, expanded: true
179
+ }
180
+ });
181
+
182
+ await wrapper.find('.section-header').trigger('click');
183
+
184
+ expect(wrapper.emitted('update:expanded')).toHaveLength(1);
185
+ expect(wrapper.emitted('update:expanded')![0]).toStrictEqual([false]);
186
+ });
187
+
188
+ it('should emit update:expanded with true when clicking a collapsed header', async() => {
189
+ const wrapper = mount(RcSection, {
190
+ props: {
191
+ ...defaultProps, expandable: true, expanded: false
192
+ }
193
+ });
194
+
195
+ await wrapper.find('.section-header').trigger('click');
196
+
197
+ expect(wrapper.emitted('update:expanded')).toHaveLength(1);
198
+ expect(wrapper.emitted('update:expanded')![0]).toStrictEqual([true]);
199
+ });
200
+
201
+ it('should not emit update:expanded when clicking a non-expandable header', async() => {
202
+ const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: false } });
203
+
204
+ await wrapper.find('.section-header').trigger('click');
205
+
206
+ expect(wrapper.emitted('update:expanded')).toBeUndefined();
207
+ });
208
+
209
+ it('should emit update:expanded when toggle button is clicked', async() => {
210
+ const wrapper = mount(RcSection, {
211
+ props: {
212
+ ...defaultProps, expandable: true, expanded: true
213
+ }
214
+ });
215
+
216
+ await wrapper.find('.toggle-button').trigger('click');
217
+
218
+ expect(wrapper.emitted('update:expanded')).toHaveLength(1);
219
+ expect(wrapper.emitted('update:expanded')![0]).toStrictEqual([false]);
220
+ });
221
+ });
222
+
223
+ describe('expanded prop', () => {
224
+ it('should default expanded to true', () => {
225
+ const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: true } });
226
+
227
+ expect(wrapper.find('.section-content').exists()).toBe(true);
228
+ });
229
+
230
+ it('should render content when expanded is true', () => {
231
+ const wrapper = mount(RcSection, {
232
+ props: { ...defaultProps, expanded: true },
233
+ slots: { default: '<p>Content</p>' },
234
+ });
235
+
236
+ expect(wrapper.find('.section-content').exists()).toBe(true);
237
+ expect(wrapper.find('p').text()).toBe('Content');
238
+ });
239
+
240
+ it('should hide content when expanded is false', () => {
241
+ const wrapper = mount(RcSection, {
242
+ props: { ...defaultProps, expanded: false },
243
+ slots: { default: '<p>Content</p>' },
244
+ });
245
+
246
+ expect(wrapper.find('.section-content').exists()).toBe(false);
247
+ });
248
+
249
+ it('should apply expandable-content class when expandable is true', () => {
250
+ const wrapper = mount(RcSection, {
251
+ props: {
252
+ ...defaultProps, expandable: true, expanded: true
253
+ }
254
+ });
255
+
256
+ expect(wrapper.find('.section-content').classes()).toContain('expandable-content');
257
+ });
258
+
259
+ it('should not apply expandable-content class when expandable is false', () => {
260
+ const wrapper = mount(RcSection, {
261
+ props: {
262
+ ...defaultProps, expandable: false, expanded: true
263
+ }
264
+ });
265
+
266
+ expect(wrapper.find('.section-content').classes()).not.toContain('expandable-content');
267
+ });
268
+
269
+ it('should add collapsed class to header when not expanded', () => {
270
+ const wrapper = mount(RcSection, {
271
+ props: {
272
+ ...defaultProps, expandable: true, expanded: false
273
+ }
274
+ });
275
+
276
+ expect(wrapper.find('.section-header').classes()).toContain('collapsed');
277
+ });
278
+ });
279
+
280
+ describe('slots', () => {
281
+ it('should render badges slot inside right-wrapper', () => {
282
+ const wrapper = mount(RcSection, {
283
+ props: { ...defaultProps },
284
+ slots: { badges: '<span class="test-badge">Badge</span>' },
285
+ });
286
+
287
+ expect(wrapper.find('.right-wrapper .status-badges .test-badge').exists()).toBe(true);
288
+ });
289
+
290
+ it('should render actions slot inside right-wrapper', () => {
291
+ const wrapper = mount(RcSection, {
292
+ props: { ...defaultProps },
293
+ slots: { actions: '<button class="test-action">Act</button>' },
294
+ });
295
+
296
+ expect(wrapper.find('.right-wrapper .actions .test-action').exists()).toBe(true);
297
+ });
298
+
299
+ it('should not render right-wrapper when no badges or actions slots', () => {
300
+ const wrapper = mount(RcSection, { props: { ...defaultProps } });
301
+
302
+ expect(wrapper.find('.right-wrapper').exists()).toBe(false);
303
+ });
304
+
305
+ it('should render counter slot', () => {
306
+ const wrapper = mount(RcSection, {
307
+ props: { ...defaultProps },
308
+ slots: { counter: '<span class="test-counter">5</span>' },
309
+ });
310
+
311
+ expect(wrapper.find('.test-counter').exists()).toBe(true);
312
+ });
313
+
314
+ it('should render errors slot', () => {
315
+ const wrapper = mount(RcSection, {
316
+ props: { ...defaultProps },
317
+ slots: { errors: '<span class="test-error">!</span>' },
318
+ });
319
+
320
+ expect(wrapper.find('.test-error').exists()).toBe(true);
321
+ });
322
+ });
323
+ });
@@ -0,0 +1,252 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * A section container used for grouping and organizing content, with an
4
+ * optional header that supports title, badges, actions, and expandability.
5
+ *
6
+ * Example:
7
+ *
8
+ * <RcSection title="Section title" type="secondary" mode="with-header" :expandable="false" background="secondary">
9
+ * <p>Section content here</p>
10
+ * </RcSection>
11
+ *
12
+ * <RcSection title="Section title" type="secondary" mode="with-header" expandable v-model:expanded="expanded" background="secondary">
13
+ * <template #counter>
14
+ * <RcCounterBadge :count="99" type="inactive" />
15
+ * </template>
16
+ * <template #errors>
17
+ * <RcIcon v-clean-tooltip="'3 validation errors'" type="error" size="large" status="error" />
18
+ * </template>
19
+ * <template #badges>
20
+ * <RcSectionBadges :badges="[
21
+ * { label: 'Status', status: 'success', tooltip: 'All systems operational' },
22
+ * { label: 'Status', status: 'warning', tooltip: 'Degraded performance' },
23
+ * { label: 'Status', status: 'error', tooltip: 'Service unavailable' },
24
+ * ]" />
25
+ * </template>
26
+ * <template #actions>
27
+ * <RcSectionActions :actions="[
28
+ * { label: 'Action', icon: 'chevron-left', action: () => {} },
29
+ * { icon: 'copy', ariaLabel: 'Copy', action: () => {} },
30
+ * { icon: 'trash', label: 'Delete', action: () => {} },
31
+ * ]" />
32
+ * </template>
33
+ * <p>Section content here</p>
34
+ * </RcSection>
35
+ */
36
+ import { computed, inject, provide, type Ref } from 'vue';
37
+ import RcButton from '@components/RcButton/RcButton.vue';
38
+ import RcIcon from '@components/RcIcon/RcIcon.vue';
39
+ import type { RcSectionProps, SectionBackground } from './types';
40
+
41
+ const RC_SECTION_BG_KEY = 'rc-section-background';
42
+
43
+ const props = withDefaults(defineProps<RcSectionProps>(), { title: '' });
44
+
45
+ const parentBackground = inject<Ref<SectionBackground> | null>(RC_SECTION_BG_KEY, null);
46
+
47
+ const resolvedBackground = computed<SectionBackground>(() => {
48
+ if (props.background) {
49
+ return props.background;
50
+ }
51
+
52
+ const parent = parentBackground?.value ?? null;
53
+
54
+ return parent === 'primary' ? 'secondary' : 'primary';
55
+ });
56
+
57
+ provide(RC_SECTION_BG_KEY, resolvedBackground);
58
+
59
+ const expanded = defineModel<boolean>('expanded', { default: true });
60
+
61
+ const hasHeader = computed(() => {
62
+ return props.mode === 'with-header';
63
+ });
64
+
65
+ const sectionClass = computed(() => ({
66
+ 'rc-section': true,
67
+ 'type-primary': props.type === 'primary',
68
+ 'type-secondary': props.type === 'secondary',
69
+ 'bg-primary': resolvedBackground.value === 'primary',
70
+ 'bg-secondary': resolvedBackground.value === 'secondary',
71
+ }));
72
+
73
+ const contentClass = computed(() => ({
74
+ 'section-content': true,
75
+ 'no-header': !hasHeader.value,
76
+ 'expandable-content': props.expandable,
77
+ }));
78
+
79
+ function toggle() {
80
+ if (props.expandable) {
81
+ expanded.value = !expanded.value;
82
+ }
83
+ }
84
+ </script>
85
+
86
+ <template>
87
+ <div :class="sectionClass">
88
+ <div
89
+ v-if="hasHeader"
90
+ class="section-header"
91
+ :class="{ expandable: props.expandable, collapsed: !expanded }"
92
+ @click="toggle"
93
+ >
94
+ <div class="left-wrapper">
95
+ <RcButton
96
+ v-if="props.expandable"
97
+ class="toggle-button"
98
+ variant="ghost"
99
+ :aria-expanded="expanded"
100
+ :aria-label="expanded ? 'Collapse section' : 'Expand section'"
101
+ @click.stop="toggle"
102
+ >
103
+ <RcIcon
104
+ :type="expanded ? 'chevron-down' : 'chevron-right'"
105
+ size="medium"
106
+ />
107
+ </RcButton>
108
+ <div class="title">
109
+ <slot name="title">
110
+ {{ props.title }}
111
+ </slot>
112
+ <slot name="counter" />
113
+ <slot name="errors" />
114
+ </div>
115
+ </div>
116
+ <div
117
+ v-if="$slots.badges || $slots.actions"
118
+ class="right-wrapper"
119
+ >
120
+ <div
121
+ v-if="$slots.badges"
122
+ class="status-badges"
123
+ >
124
+ <slot name="badges" />
125
+ </div>
126
+ <div
127
+ v-if="$slots.actions"
128
+ class="actions"
129
+ @click.stop
130
+ >
131
+ <slot name="actions" />
132
+ </div>
133
+ </div>
134
+ </div>
135
+ <div
136
+ v-if="expanded"
137
+ :class="contentClass"
138
+ >
139
+ <slot />
140
+ </div>
141
+ </div>
142
+ </template>
143
+
144
+ <style lang="scss" scoped>
145
+ .rc-section {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 8px;
149
+
150
+ &.type-primary {
151
+ .title {
152
+ font-weight: 700;
153
+ }
154
+ }
155
+
156
+ &.type-secondary {
157
+ padding: 0 16px;
158
+ border-radius: 8px;
159
+
160
+ .title {
161
+ font-weight: 600;
162
+ }
163
+ }
164
+
165
+ &.bg-primary {
166
+ background-color: var(--rc-section-background-primary);
167
+ }
168
+
169
+ &.bg-secondary {
170
+ background-color: var(--rc-section-background-secondary);
171
+ }
172
+ }
173
+
174
+ .section-header {
175
+ display: flex;
176
+ flex-direction: row;
177
+ align-items: center;
178
+ gap: 24px;
179
+ height: 56px;
180
+
181
+ &.expandable {
182
+ cursor: pointer;
183
+ user-select: none;
184
+ }
185
+ }
186
+
187
+ .left-wrapper {
188
+ display: flex;
189
+ flex-direction: row;
190
+ align-items: center;
191
+ gap: 12px;
192
+
193
+ .toggle-button + .title {
194
+ margin-left: -4px;
195
+ }
196
+ }
197
+
198
+ .title {
199
+ display: inline-flex;
200
+ gap: 12px;
201
+ font-size: 18px;
202
+ line-height: 1.2;
203
+ color: var(--body-text, inherit);
204
+ }
205
+
206
+ button.btn-medium.toggle-button {
207
+ flex-shrink: 0;
208
+ font-size: 16px;
209
+ color: var(--body-text, inherit);
210
+ padding: 0;
211
+ min-height: initial;
212
+ }
213
+
214
+ .right-wrapper {
215
+ display: flex;
216
+ flex-direction: row;
217
+ justify-content: flex-end;
218
+ align-items: center;
219
+ gap: 24px;
220
+ margin-left: auto;
221
+ }
222
+
223
+ .status-badges {
224
+ display: flex;
225
+ flex-direction: row;
226
+ align-items: center;
227
+ gap: 12px;
228
+ }
229
+
230
+ .actions {
231
+ display: flex;
232
+ flex-direction: row;
233
+ align-items: center;
234
+ gap: 0;
235
+ }
236
+
237
+ .section-content {
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: 24px;
241
+ padding: 0 0 16px;
242
+ color: var(--body-text);
243
+
244
+ &.expandable-content {
245
+ padding: 0 0 16px 24px;
246
+ }
247
+
248
+ &.no-header {
249
+ padding: 16px 0;
250
+ }
251
+ }
252
+ </style>