@openmfp/webcomponents 0.6.1

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 (140) hide show
  1. package/.github/workflows/pipeline.yaml +41 -0
  2. package/.storybook/main.ts +34 -0
  3. package/.storybook/preview.ts +29 -0
  4. package/.storybook/tsconfig.json +11 -0
  5. package/AGENTS.md +153 -0
  6. package/CODEOWNERS +6 -0
  7. package/CONTRIBUTING.md +95 -0
  8. package/LICENSE +201 -0
  9. package/LICENSES/Apache-2.0.txt +73 -0
  10. package/README.md +91 -0
  11. package/REUSE.toml +9 -0
  12. package/angular.json +157 -0
  13. package/docs/dashboard.md +358 -0
  14. package/docs/declarative-form.md +178 -0
  15. package/docs/declarative-table-card.md +235 -0
  16. package/docs/declarative-table.md +315 -0
  17. package/eslint.config.js +41 -0
  18. package/package.json +73 -0
  19. package/projects/ngx/cards/favorites/favorites.component.html +12 -0
  20. package/projects/ngx/cards/favorites/favorites.component.scss +50 -0
  21. package/projects/ngx/cards/favorites/favorites.component.ts +19 -0
  22. package/projects/ngx/cards/public-api.ts +4 -0
  23. package/projects/ngx/cards/service-status/service-status-card.component.html +15 -0
  24. package/projects/ngx/cards/service-status/service-status-card.component.scss +87 -0
  25. package/projects/ngx/cards/service-status/service-status-card.component.ts +36 -0
  26. package/projects/ngx/cards/stories/visited-service-card.stories.ts +149 -0
  27. package/projects/ngx/cards/visited-service-card/visited-service-card.component.html +17 -0
  28. package/projects/ngx/cards/visited-service-card/visited-service-card.component.scss +34 -0
  29. package/projects/ngx/cards/visited-service-card/visited-service-card.component.ts +22 -0
  30. package/projects/ngx/cards/whats-new/whats-new.component.html +10 -0
  31. package/projects/ngx/cards/whats-new/whats-new.component.scss +25 -0
  32. package/projects/ngx/cards/whats-new/whats-new.component.ts +46 -0
  33. package/projects/ngx/declarative-ui/dashboard/add-card-dialog/add-card-dialog.component.html +28 -0
  34. package/projects/ngx/declarative-ui/dashboard/add-card-dialog/add-card-dialog.component.scss +44 -0
  35. package/projects/ngx/declarative-ui/dashboard/add-card-dialog/add-card-dialog.component.spec.ts +85 -0
  36. package/projects/ngx/declarative-ui/dashboard/add-card-dialog/add-card-dialog.component.ts +58 -0
  37. package/projects/ngx/declarative-ui/dashboard/card/dashboard-card.component.html +29 -0
  38. package/projects/ngx/declarative-ui/dashboard/card/dashboard-card.component.scss +63 -0
  39. package/projects/ngx/declarative-ui/dashboard/card/dashboard-card.component.spec.ts +255 -0
  40. package/projects/ngx/declarative-ui/dashboard/card/dashboard-card.component.ts +75 -0
  41. package/projects/ngx/declarative-ui/dashboard/card/utils/dashboard-card-registry.spec.ts +76 -0
  42. package/projects/ngx/declarative-ui/dashboard/card/utils/dashboard-card-registry.ts +109 -0
  43. package/projects/ngx/declarative-ui/dashboard/card/utils/index.ts +4 -0
  44. package/projects/ngx/declarative-ui/dashboard/card/utils/mount-angular-card.spec.ts +141 -0
  45. package/projects/ngx/declarative-ui/dashboard/card/utils/mount-angular-card.ts +44 -0
  46. package/projects/ngx/declarative-ui/dashboard/card/utils/mount-sap-card.spec.ts +142 -0
  47. package/projects/ngx/declarative-ui/dashboard/card/utils/mount-sap-card.ts +52 -0
  48. package/projects/ngx/declarative-ui/dashboard/card/utils/mount-wc-card.spec.ts +107 -0
  49. package/projects/ngx/declarative-ui/dashboard/card/utils/mount-wc-card.ts +22 -0
  50. package/projects/ngx/declarative-ui/dashboard/dashboard/dashboard.component.html +134 -0
  51. package/projects/ngx/declarative-ui/dashboard/dashboard/dashboard.component.scss +88 -0
  52. package/projects/ngx/declarative-ui/dashboard/dashboard/dashboard.component.spec.ts +354 -0
  53. package/projects/ngx/declarative-ui/dashboard/dashboard/dashboard.component.ts +238 -0
  54. package/projects/ngx/declarative-ui/dashboard/dashboard/index.ts +1 -0
  55. package/projects/ngx/declarative-ui/dashboard/index.ts +5 -0
  56. package/projects/ngx/declarative-ui/dashboard/models/constants.ts +2 -0
  57. package/projects/ngx/declarative-ui/dashboard/models/dashboard.model.ts +50 -0
  58. package/projects/ngx/declarative-ui/dashboard/models/index.ts +1 -0
  59. package/projects/ngx/declarative-ui/dashboard/section/dashboard-section.component.html +28 -0
  60. package/projects/ngx/declarative-ui/dashboard/section/dashboard-section.component.scss +85 -0
  61. package/projects/ngx/declarative-ui/dashboard/section/dashboard-section.component.spec.ts +104 -0
  62. package/projects/ngx/declarative-ui/dashboard/section/dashboard-section.component.ts +23 -0
  63. package/projects/ngx/declarative-ui/form/declarative-form/declarative-form.component.html +62 -0
  64. package/projects/ngx/declarative-ui/form/declarative-form/declarative-form.component.scss +12 -0
  65. package/projects/ngx/declarative-ui/form/declarative-form/declarative-form.component.spec.ts +301 -0
  66. package/projects/ngx/declarative-ui/form/declarative-form/declarative-form.component.ts +166 -0
  67. package/projects/ngx/declarative-ui/form/declarative-form/index.ts +1 -0
  68. package/projects/ngx/declarative-ui/form/index.ts +2 -0
  69. package/projects/ngx/declarative-ui/form/models/form-field-definition.ts +15 -0
  70. package/projects/ngx/declarative-ui/form/models/index.ts +1 -0
  71. package/projects/ngx/declarative-ui/form/utils/set-property-by-path.ts +30 -0
  72. package/projects/ngx/declarative-ui/models/index.ts +2 -0
  73. package/projects/ngx/declarative-ui/models/resource.ts +5 -0
  74. package/projects/ngx/declarative-ui/models/ui-definition.ts +95 -0
  75. package/projects/ngx/declarative-ui/public-api.ts +4 -0
  76. package/projects/ngx/declarative-ui/stories/add-card-dialog.stories.ts +91 -0
  77. package/projects/ngx/declarative-ui/stories/background-lightblue.png +0 -0
  78. package/projects/ngx/declarative-ui/stories/dashboard.cards.ts +107 -0
  79. package/projects/ngx/declarative-ui/stories/dashboard.stories.ts +296 -0
  80. package/projects/ngx/declarative-ui/stories/declarative-form.stories.ts +149 -0
  81. package/projects/ngx/declarative-ui/stories/declarative-table-card.stories.ts +358 -0
  82. package/projects/ngx/declarative-ui/stories/declarative-table.stories.ts +363 -0
  83. package/projects/ngx/declarative-ui/stories/pods-table.config.ts +188 -0
  84. package/projects/ngx/declarative-ui/table/declarative-table/declarative-table.component.html +138 -0
  85. package/projects/ngx/declarative-ui/table/declarative-table/declarative-table.component.scss +21 -0
  86. package/projects/ngx/declarative-ui/table/declarative-table/declarative-table.component.spec.ts +345 -0
  87. package/projects/ngx/declarative-ui/table/declarative-table/declarative-table.component.ts +61 -0
  88. package/projects/ngx/declarative-ui/table/declarative-table/index.ts +1 -0
  89. package/projects/ngx/declarative-ui/table/index.ts +2 -0
  90. package/projects/ngx/declarative-ui/table/models/index.ts +14 -0
  91. package/projects/ngx/declarative-ui/table/models/table.model.ts +17 -0
  92. package/projects/ngx/declarative-ui/table/utils/cssRules.engine.spec.ts +146 -0
  93. package/projects/ngx/declarative-ui/table/utils/cssRules.engine.ts +69 -0
  94. package/projects/ngx/declarative-ui/table/utils/field-definition.utils.spec.ts +70 -0
  95. package/projects/ngx/declarative-ui/table/utils/field-definition.utils.ts +13 -0
  96. package/projects/ngx/declarative-ui/table/utils/proccess-fields.spec.ts +511 -0
  97. package/projects/ngx/declarative-ui/table/utils/proccess-fields.ts +71 -0
  98. package/projects/ngx/declarative-ui/table/utils/resource-field-by-path.spec.ts +372 -0
  99. package/projects/ngx/declarative-ui/table/utils/resource-field-by-path.ts +98 -0
  100. package/projects/ngx/declarative-ui/table/value-cell/boolean-value/boolean-cell.constants.ts +5 -0
  101. package/projects/ngx/declarative-ui/table/value-cell/boolean-value/boolean-value.component.html +1 -0
  102. package/projects/ngx/declarative-ui/table/value-cell/boolean-value/boolean-value.component.scss +0 -0
  103. package/projects/ngx/declarative-ui/table/value-cell/boolean-value/boolean-value.component.spec.ts +119 -0
  104. package/projects/ngx/declarative-ui/table/value-cell/boolean-value/boolean-value.component.ts +35 -0
  105. package/projects/ngx/declarative-ui/table/value-cell/link-value/link-value.component.html +7 -0
  106. package/projects/ngx/declarative-ui/table/value-cell/link-value/link-value.component.scss +0 -0
  107. package/projects/ngx/declarative-ui/table/value-cell/link-value/link-value.component.spec.ts +114 -0
  108. package/projects/ngx/declarative-ui/table/value-cell/link-value/link-value.component.ts +19 -0
  109. package/projects/ngx/declarative-ui/table/value-cell/secret-value/secret-value.component.html +7 -0
  110. package/projects/ngx/declarative-ui/table/value-cell/secret-value/secret-value.component.scss +10 -0
  111. package/projects/ngx/declarative-ui/table/value-cell/secret-value/secret-value.component.spec.ts +188 -0
  112. package/projects/ngx/declarative-ui/table/value-cell/secret-value/secret-value.component.ts +16 -0
  113. package/projects/ngx/declarative-ui/table/value-cell/value-cell.component.html +59 -0
  114. package/projects/ngx/declarative-ui/table/value-cell/value-cell.component.scss +33 -0
  115. package/projects/ngx/declarative-ui/table/value-cell/value-cell.component.spec.ts +316 -0
  116. package/projects/ngx/declarative-ui/table/value-cell/value-cell.component.ts +115 -0
  117. package/projects/ngx/declarative-ui/table-card/declarative-table-card.component.html +156 -0
  118. package/projects/ngx/declarative-ui/table-card/declarative-table-card.component.scss +123 -0
  119. package/projects/ngx/declarative-ui/table-card/declarative-table-card.component.spec.ts +786 -0
  120. package/projects/ngx/declarative-ui/table-card/declarative-table-card.component.ts +286 -0
  121. package/projects/ngx/declarative-ui/table-card/index.ts +2 -0
  122. package/projects/ngx/declarative-ui/table-card/models/configs.ts +46 -0
  123. package/projects/ngx/declarative-ui/tsconfig.lib.json +11 -0
  124. package/projects/ngx/declarative-ui/tsconfig.lib.prod.json +9 -0
  125. package/projects/ngx/declarative-ui/tsconfig.spec.json +9 -0
  126. package/projects/ngx/ng-package.json +8 -0
  127. package/projects/ngx/package.json +22 -0
  128. package/projects/ngx/public-api.ts +2 -0
  129. package/projects/ngx/tsconfig.lib.json +11 -0
  130. package/projects/ngx/tsconfig.lib.prod.json +9 -0
  131. package/projects/webcomponents/main.ts +92 -0
  132. package/projects/webcomponents/tsconfig.app.json +9 -0
  133. package/projects/webcomponents-dashboard/main.ts +15 -0
  134. package/projects/webcomponents-dashboard/tsconfig.app.json +9 -0
  135. package/renovate.json +6 -0
  136. package/scripts/bundle-wc.mjs +79 -0
  137. package/tsconfig.json +37 -0
  138. package/tsconfig.spec.json +8 -0
  139. package/tsconfig.storybook.json +16 -0
  140. package/vitest.config.ts +26 -0
@@ -0,0 +1,149 @@
1
+ import { VisitedServiceCard } from '../visited-service-card/visited-service-card.component';
2
+ import {
3
+ CUSTOM_ELEMENTS_SCHEMA,
4
+ Component,
5
+ EventEmitter,
6
+ Input,
7
+ Output,
8
+ } from '@angular/core';
9
+ import { MessageStrip } from '@fundamental-ngx/ui5-webcomponents/message-strip';
10
+ import type { Meta, StoryObj } from '@storybook/angular';
11
+
12
+ @Component({
13
+ selector: 'visited-service-card-story',
14
+ template: `
15
+ <mfp-visited-service-card
16
+ [serviceType]="serviceType"
17
+ [serviceName]="serviceName"
18
+ [serviceIcon]="serviceIcon"
19
+ [serviceDescription]="serviceDescription"
20
+ [path]="path"
21
+ (click)="onCardClick()"
22
+ ></mfp-visited-service-card>
23
+ @if (clicked) {
24
+ <ui5-message-strip design="Information" style="margin-top: 1rem;">
25
+ Card clicked — would navigate to: {{ path }}
26
+ </ui5-message-strip>
27
+ }
28
+ `,
29
+ imports: [MessageStrip, VisitedServiceCard],
30
+ })
31
+ class VisitedServiceCardStory {
32
+ @Input() serviceType = '';
33
+ @Input() serviceName = '';
34
+ @Input() serviceIcon = '';
35
+ @Input() serviceDescription = '';
36
+ @Input() path = '';
37
+ @Output() click = new EventEmitter<string>();
38
+ clicked = false;
39
+ onCardClick() {
40
+ this.clicked = true;
41
+ setTimeout(() => (this.clicked = false), 3000);
42
+ }
43
+ }
44
+
45
+ const meta: Meta<VisitedServiceCardStory> = {
46
+ title: 'Cards / VisitedServiceCard',
47
+ component: VisitedServiceCardStory,
48
+ tags: ['autodocs'],
49
+ parameters: { layout: 'padded' },
50
+ argTypes: {
51
+ serviceType: { control: 'text' },
52
+ serviceName: { control: 'text' },
53
+ serviceIcon: { control: 'text' },
54
+ serviceDescription: { control: 'text' },
55
+ path: { control: 'text' },
56
+ },
57
+ };
58
+
59
+ export default meta;
60
+ type Story = StoryObj<VisitedServiceCardStory>;
61
+
62
+ export const HanaCloud: Story = {
63
+ args: {
64
+ serviceType: 'SAP HANA Cloud',
65
+ serviceName: 'olc-hana-db',
66
+ serviceIcon: 'database',
67
+ serviceDescription: 'My Subaccount 1/Space dev',
68
+ path: '/hana/olc-hana-db',
69
+ },
70
+ };
71
+
72
+ export const CloudIdentityService: Story = {
73
+ args: {
74
+ serviceType: 'Cloud Identity Service',
75
+ serviceName: 'Cloud Identity Service',
76
+ serviceIcon: 'customer',
77
+ serviceDescription: 'My Subaccount 1/Space dev',
78
+ path: '/identity/cloud-identity-service',
79
+ },
80
+ };
81
+
82
+ export const HanaCloudTest: Story = {
83
+ args: {
84
+ serviceType: 'SAP HANA Cloud',
85
+ serviceName: 'olc-hana-db-test',
86
+ serviceIcon: 'database',
87
+ serviceDescription: 'My Subaccount 1/Space dev',
88
+ path: '/hana/olc-hana-db-test',
89
+ },
90
+ };
91
+
92
+ export const ApplicationAutoscaler: Story = {
93
+ args: {
94
+ serviceType: 'Application Autoscaler',
95
+ serviceName: 'applicationtest',
96
+ serviceIcon: 'accelerated',
97
+ serviceDescription: 'My Subaccount 2/Space prod',
98
+ path: '/autoscaler/applicationtest',
99
+ },
100
+ };
101
+
102
+ export const CloudIdentityLongPath: Story = {
103
+ args: {
104
+ serviceType: 'Cloud Identity Service',
105
+ serviceName: 'Cloud Identity Service',
106
+ serviceIcon: 'customer',
107
+ serviceDescription: 'Long text Subaccount 1/Space',
108
+ path: '/identity/cloud-identity-service',
109
+ },
110
+ };
111
+
112
+ export const AuditLogService: Story = {
113
+ args: {
114
+ serviceType: 'Audit Log Service',
115
+ serviceName: 'auditlog-name',
116
+ serviceIcon: 'log',
117
+ serviceDescription: 'My Subaccount 4/Space dev',
118
+ path: '/auditlog/auditlog-name',
119
+ },
120
+ };
121
+
122
+ export const AllCards: Story = {
123
+ render: () => ({
124
+ props: {
125
+ clicked: false,
126
+ clickedPath: '',
127
+ onCardClick(path: string) {
128
+ this['clickedPath'] = path;
129
+ this['clicked'] = true;
130
+ setTimeout(() => (this['clicked'] = false), 3000);
131
+ },
132
+ },
133
+ template: `
134
+ <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; padding: 1rem;">
135
+ <mfp-visited-service-card serviceType="SAP HANA Cloud" serviceName="olc-hana-db" serviceIcon="database" serviceDescription="My Subaccount 1/Space dev" path="/hana/olc-hana-db" (click)="onCardClick('/hana/olc-hana-db')"></mfp-visited-service-card>
136
+ <mfp-visited-service-card serviceType="Cloud Identity Service" serviceName="Cloud Identity Service" serviceIcon="customer" serviceDescription="My Subaccount 1/Space dev" path="/identity/cloud-identity-service" (click)="onCardClick('/identity/cloud-identity-service')"></mfp-visited-service-card>
137
+ <mfp-visited-service-card serviceType="SAP HANA Cloud" serviceName="olc-hana-db-test" serviceIcon="database" serviceDescription="My Subaccount 1/Space dev" path="/hana/olc-hana-db-test" (click)="onCardClick('/hana/olc-hana-db-test')"></mfp-visited-service-card>
138
+ <mfp-visited-service-card serviceType="Application Autoscaler" serviceName="applicationtest" serviceIcon="accelerated" serviceDescription="My Subaccount 2/Space prod" path="/autoscaler/applicationtest" (click)="onCardClick('/autoscaler/applicationtest')"></mfp-visited-service-card>
139
+ <mfp-visited-service-card serviceType="Cloud Identity Service" serviceName="Cloud Identity Service" serviceIcon="customer" serviceDescription="Long text Subaccount 1/Space" path="/identity/cloud-identity-service" (click)="onCardClick('/identity/cloud-identity-service')"></mfp-visited-service-card>
140
+ <mfp-visited-service-card serviceType="Audit Log Service" serviceName="auditlog-name" serviceIcon="log" serviceDescription="My Subaccount 4/Space dev" path="/auditlog/auditlog-name" (click)="onCardClick('/auditlog/auditlog-name')"></mfp-visited-service-card>
141
+ </div>
142
+ @if (clicked) {
143
+ <ui5-message-strip design="Information" style="margin-top: 1rem;">
144
+ Card clicked — would navigate to: {{ clickedPath }}
145
+ </ui5-message-strip>
146
+ }
147
+ `,
148
+ }),
149
+ };
@@ -0,0 +1,17 @@
1
+ <div class="visited-card-wrapper" (click)="click.emit(path())">
2
+ <span class="visited-card__type-badge">{{ serviceType() }}</span>
3
+ <ui5-card class="visited-card">
4
+ <ui5-card-header
5
+ slot="header"
6
+ [titleText]="serviceName()"
7
+ [subtitleText]="serviceDescription()"
8
+ [interactive]="true"
9
+ >
10
+ <ui5-icon
11
+ slot="avatar"
12
+ [name]="serviceIcon()"
13
+ class="visited-card__icon"
14
+ ></ui5-icon>
15
+ </ui5-card-header>
16
+ </ui5-card>
17
+ </div>
@@ -0,0 +1,34 @@
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ .visited-card-wrapper {
6
+ position: relative;
7
+ padding-top: 0.625rem;
8
+ cursor: pointer;
9
+ }
10
+
11
+ .visited-card {
12
+ width: 100%;
13
+ }
14
+
15
+ .visited-card__type-badge {
16
+ position: absolute;
17
+ top: 0;
18
+ right: 0.75rem;
19
+ display: inline-block;
20
+ padding: 0.125rem 0.625rem;
21
+ border-radius: 0.75rem;
22
+ background-color: #e8f3ff;
23
+ color: #0070f2;
24
+ font-size: var(--sapFontSmallSize, 0.75rem);
25
+ font-weight: 700;
26
+ white-space: nowrap;
27
+ z-index: 1;
28
+ }
29
+
30
+ .visited-card__icon {
31
+ width: 2.5rem;
32
+ height: 2.5rem;
33
+ color: var(--sapHighlightColor, #0070f2);
34
+ }
@@ -0,0 +1,22 @@
1
+ import { Component, ViewEncapsulation, input, output } from '@angular/core';
2
+ import { Card } from '@fundamental-ngx/ui5-webcomponents/card';
3
+ import { CardHeader } from '@fundamental-ngx/ui5-webcomponents/card-header';
4
+ import { Icon } from '@fundamental-ngx/ui5-webcomponents/icon';
5
+ import '@ui5/webcomponents-icons/dist/AllIcons.js';
6
+
7
+ @Component({
8
+ selector: 'mfp-visited-service-card',
9
+ templateUrl: './visited-service-card.component.html',
10
+ styleUrls: ['./visited-service-card.component.scss'],
11
+ encapsulation: ViewEncapsulation.ShadowDom,
12
+ imports: [Card, CardHeader, Icon],
13
+ })
14
+ export class VisitedServiceCard {
15
+ serviceType = input.required<string>();
16
+ serviceName = input.required<string>();
17
+ serviceDescription = input.required<string>();
18
+ serviceIcon = input.required<string>();
19
+ path = input.required<string>();
20
+
21
+ click = output<string>();
22
+ }
@@ -0,0 +1,10 @@
1
+ <div class="whats-new">
2
+ <ui5-title level="H5" class="whats-new__title">What's New</ui5-title>
3
+ <ui5-list separators="Inner" class="whats-new__list">
4
+ @for (item of headlines; track item.title) {
5
+ <ui5-li icon="{{ item.icon }}" description="{{ item.description }}">
6
+ {{ item.title }}
7
+ </ui5-li>
8
+ }
9
+ </ui5-list>
10
+ </div>
@@ -0,0 +1,25 @@
1
+ :host {
2
+ display: block;
3
+ height: 100%;
4
+ }
5
+
6
+ .whats-new {
7
+ display: flex;
8
+ flex-direction: column;
9
+ height: 100%;
10
+ background: var(--sapTile_Background, #fff);
11
+ border-radius: var(--_ui5_card_border-radius, 0.5rem);
12
+ border: var(--_ui5_card_border, 1px solid var(--sapTile_BorderColor, #e5e5e5));
13
+ box-shadow: var(--_ui5_card_box_shadow, var(--sapContent_Shadow0));
14
+ overflow: hidden;
15
+
16
+ &__title {
17
+ padding: 1rem 1rem 0.5rem;
18
+ flex-shrink: 0;
19
+ }
20
+
21
+ &__list {
22
+ flex: 1;
23
+ overflow-y: auto;
24
+ }
25
+ }
@@ -0,0 +1,46 @@
1
+ import { Component, ViewEncapsulation } from '@angular/core';
2
+ import { List } from '@fundamental-ngx/ui5-webcomponents/list';
3
+ import { ListItemStandard } from '@fundamental-ngx/ui5-webcomponents/list-item-standard';
4
+ import { Title } from '@fundamental-ngx/ui5-webcomponents/title';
5
+
6
+ @Component({
7
+ selector: 'mfp-whats-new',
8
+ templateUrl: './whats-new.component.html',
9
+ styleUrls: ['./whats-new.component.scss'],
10
+ encapsulation: ViewEncapsulation.ShadowDom,
11
+ imports: [List, ListItemStandard, Title],
12
+ })
13
+ export class WhatsNew {
14
+ readonly headlines = [
15
+ {
16
+ title: 'Kubernetes 1.32 Released',
17
+ description: 'Improved scheduling, new APIs and graduated features.',
18
+ icon: 'cloud',
19
+ },
20
+ {
21
+ title: 'Angular 20 Signals Stable',
22
+ description: 'Signal-based reactivity is now production-ready.',
23
+ icon: 'developer-settings',
24
+ },
25
+ {
26
+ title: 'WebAssembly WASI 2.0 Preview',
27
+ description: 'System interface advances bring server-side WASM closer.',
28
+ icon: 'technical-object',
29
+ },
30
+ {
31
+ title: 'OpenTelemetry Hits 1.0',
32
+ description: 'Unified observability standard lands in enterprise stacks.',
33
+ icon: 'monitor-payments',
34
+ },
35
+ {
36
+ title: 'Rust Enters Linux Kernel Mainstream',
37
+ description: 'More subsystems accept Rust driver contributions.',
38
+ icon: 'settings',
39
+ },
40
+ {
41
+ title: 'TypeScript 5.8 Performance Boost',
42
+ description: 'Declaration emit is up to 10x faster in large projects.',
43
+ icon: 'accelerated',
44
+ },
45
+ ];
46
+ }
@@ -0,0 +1,28 @@
1
+ <ui5-dialog [open]="open()" (ui5BeforeClose)="cancel.emit()">
2
+ <div slot="header" class="add-card-dialog__header">
3
+ <ui5-title level="H5">Add Card</ui5-title>
4
+ </div>
5
+ <div class="add-card-dialog">
6
+ @if (availableCards().length === 0) {
7
+ <p class="add-card-dialog__empty">No cards available.</p>
8
+ }
9
+ @for (ac of availableCards(); track ac.id) {
10
+ @let alreadyAdded = addedCardsIds().has(ac.id);
11
+ <div
12
+ class="add-card-dialog__item"
13
+ [class.add-card-dialog__item--disabled]="alreadyAdded"
14
+ >
15
+ <ui5-checkbox
16
+ [checked]="alreadyAdded || selectedIds().has(ac.id)"
17
+ [disabled]="alreadyAdded"
18
+ (ui5Change)="toggle(ac.id)"
19
+ [text]="ac.label || ac.component"
20
+ />
21
+ </div>
22
+ }
23
+ </div>
24
+ <div slot="footer" class="add-card-dialog__footer">
25
+ <ui5-button design="Emphasized" (click)="confirmAdd()">Add</ui5-button>
26
+ <ui5-button design="Transparent" (click)="cancel.emit()">Cancel</ui5-button>
27
+ </div>
28
+ </ui5-dialog>
@@ -0,0 +1,44 @@
1
+ :host {
2
+ display: contents;
3
+ }
4
+
5
+ .add-card-dialog {
6
+ margin: -1rem;
7
+ min-width: 300px;
8
+
9
+ &__header {
10
+ display: flex;
11
+ align-items: flex-start;
12
+ padding: 0.75rem 1rem;
13
+ }
14
+
15
+ &__item {
16
+ display: flex;
17
+ align-items: center;
18
+ padding: 0.5rem 1rem;
19
+ border-bottom: 1px solid var(--sapList_BorderColor, #e5e5e5);
20
+
21
+ &:last-child {
22
+ border-bottom: none;
23
+ }
24
+
25
+ &--disabled {
26
+ opacity: 0.8;
27
+ }
28
+ }
29
+
30
+ &__empty {
31
+ padding: 1rem;
32
+ margin: 0;
33
+ color: var(--sapContent_LabelColor, #6a6d70);
34
+ font-size: 0.875rem;
35
+ }
36
+ }
37
+
38
+ .add-card-dialog__footer {
39
+ display: flex;
40
+ justify-content: flex-end;
41
+ gap: 0.5rem;
42
+ padding: 0.5rem 1rem;
43
+ margin: 0 -1rem;
44
+ }
@@ -0,0 +1,85 @@
1
+ import { CardConfig } from '../models';
2
+ import { AddCardDialog } from './add-card-dialog.component';
3
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
4
+
5
+ type Fixture = ComponentFixture<AddCardDialog>;
6
+
7
+ function setup(): { fixture: Fixture; component: AddCardDialog } {
8
+ const fixture = TestBed.createComponent(AddCardDialog);
9
+ const component = fixture.componentInstance;
10
+ return { fixture, component };
11
+ }
12
+
13
+ function root(fixture: Fixture): ShadowRoot | HTMLElement {
14
+ return fixture.nativeElement.shadowRoot ?? fixture.nativeElement;
15
+ }
16
+
17
+ describe('AddCardDialog', () => {
18
+ beforeEach(async () => {
19
+ await TestBed.configureTestingModule({
20
+ imports: [AddCardDialog],
21
+ }).compileComponents();
22
+ });
23
+
24
+ it('renders the empty state when there are no available cards', () => {
25
+ const { fixture } = setup();
26
+
27
+ fixture.componentRef.setInput('open', true);
28
+ fixture.componentRef.setInput('availableCards', []);
29
+ fixture.detectChanges();
30
+
31
+ expect(root(fixture).textContent).toContain('No cards available.');
32
+ });
33
+
34
+ it('resets selected cards whenever the dialog is opened', () => {
35
+ const { fixture, component } = setup();
36
+
37
+ fixture.componentRef.setInput('availableCards', [
38
+ { id: 'card-1', component: 'mfp-a' },
39
+ ]);
40
+ fixture.detectChanges();
41
+
42
+ component.toggle('card-1');
43
+ expect(component.selectedIds().has('card-1')).toBe(true);
44
+
45
+ fixture.componentRef.setInput('open', true);
46
+ fixture.detectChanges();
47
+
48
+ expect(component.selectedIds().size).toBe(0);
49
+ });
50
+
51
+ it('emits only selected cards that have not already been added', () => {
52
+ const { fixture, component } = setup();
53
+ const availableCards: CardConfig[] = [
54
+ { id: 'card-1', component: 'mfp-a', label: 'Card A' },
55
+ { id: 'card-2', component: 'mfp-b', label: 'Card B' },
56
+ ];
57
+ const emitted: CardConfig[][] = [];
58
+
59
+ component.confirm.subscribe((cards) => emitted.push(cards));
60
+ fixture.componentRef.setInput('availableCards', availableCards);
61
+ fixture.componentRef.setInput('addedCardsIds', new Set(['card-2']));
62
+ fixture.detectChanges();
63
+
64
+ component.toggle('card-1');
65
+ component.toggle('card-2');
66
+ component.confirmAdd();
67
+
68
+ expect(emitted).toEqual([[availableCards[0]]]);
69
+ });
70
+
71
+ it('emits cancel when the cancel button is clicked', () => {
72
+ const { fixture, component } = setup();
73
+ let emitted = 0;
74
+
75
+ component.cancel.subscribe(() => emitted++);
76
+ fixture.componentRef.setInput('open', true);
77
+ fixture.componentRef.setInput('availableCards', []);
78
+ fixture.detectChanges();
79
+
80
+ const buttons = root(fixture).querySelectorAll('ui5-button');
81
+ buttons[1]?.dispatchEvent(new Event('click'));
82
+
83
+ expect(emitted).toBe(1);
84
+ });
85
+ });
@@ -0,0 +1,58 @@
1
+ import { CardConfig } from '../models';
2
+ import {
3
+ Component,
4
+ ViewEncapsulation,
5
+ effect,
6
+ input,
7
+ output,
8
+ signal,
9
+ } from '@angular/core';
10
+ import { Button } from '@fundamental-ngx/ui5-webcomponents/button';
11
+ import { CheckBox } from '@fundamental-ngx/ui5-webcomponents/check-box';
12
+ import { Dialog } from '@fundamental-ngx/ui5-webcomponents/dialog';
13
+ import { Title } from '@fundamental-ngx/ui5-webcomponents/title';
14
+
15
+ @Component({
16
+ selector: 'mfp-add-card-dialog',
17
+ templateUrl: './add-card-dialog.component.html',
18
+ styleUrls: ['./add-card-dialog.component.scss'],
19
+ encapsulation: ViewEncapsulation.ShadowDom,
20
+ imports: [Button, CheckBox, Dialog, Title],
21
+ })
22
+ export class AddCardDialog {
23
+ availableCards = input<CardConfig[]>([]);
24
+ addedCardsIds = input<Set<string>>(new Set());
25
+ open = input<boolean>(false);
26
+
27
+ confirm = output<CardConfig[]>();
28
+ cancel = output<void>();
29
+
30
+ selectedIds = signal<Set<string>>(new Set());
31
+
32
+ constructor() {
33
+ effect(() => {
34
+ if (this.open()) {
35
+ this.selectedIds.set(new Set());
36
+ }
37
+ });
38
+ }
39
+
40
+ toggle(id: string): void {
41
+ this.selectedIds.update((set) => {
42
+ const next = new Set(set);
43
+ if (next.has(id)) {
44
+ next.delete(id);
45
+ } else {
46
+ next.add(id);
47
+ }
48
+ return next;
49
+ });
50
+ }
51
+
52
+ confirmAdd(): void {
53
+ const toAdd = this.availableCards().filter(
54
+ (ac) => this.selectedIds().has(ac.id) && !this.addedCardsIds().has(ac.id),
55
+ );
56
+ this.confirm.emit(toAdd);
57
+ }
58
+ }
@@ -0,0 +1,29 @@
1
+ @if (card().component) {
2
+ <div class="component-card">
3
+ @if (editMode()) {
4
+ <div class="remove-wrapper">
5
+ <ui5-button
6
+ class="card__remove"
7
+ design="Default"
8
+ icon="decline"
9
+ (click)="removeCard.emit()"
10
+ />
11
+ </div>
12
+ }
13
+ <div
14
+ [style.pointer-events]="editMode() ? 'none' : 'auto'"
15
+ class="component-host"
16
+ >
17
+ <div #elementHost></div>
18
+ </div>
19
+ </div>
20
+ } @else {
21
+ <div class="card">
22
+ <div
23
+ [style.pointer-events]="editMode() ? 'none' : 'auto'"
24
+ class="card__body"
25
+ >
26
+ <ng-content />
27
+ </div>
28
+ </div>
29
+ }
@@ -0,0 +1,63 @@
1
+ :host {
2
+ display: block;
3
+ min-width: 0;
4
+ min-height: 0;
5
+ overflow: visible;
6
+ }
7
+
8
+ .card__remove {
9
+ position: absolute;
10
+ top: 0;
11
+ right: -0.5rem;
12
+ z-index: 10;
13
+ border-radius: 50% !important;
14
+ width: 1.25rem !important;
15
+ height: 1.25rem !important;
16
+ min-width: unset !important;
17
+ padding: 0 !important;
18
+ font-size: 0.625rem !important;
19
+ }
20
+
21
+ .remove-wrapper {
22
+ position: sticky;
23
+ height: 0.5rem;
24
+ top: 0;
25
+ right: 0;
26
+ z-index: 10;
27
+ }
28
+
29
+ .component-card {
30
+ position: relative;
31
+ height: 100%;
32
+ overflow: visible;
33
+ isolation: isolate;
34
+ padding: 0 0.5rem;
35
+ }
36
+
37
+ .component-host {
38
+ height: 100%;
39
+ }
40
+
41
+ .card {
42
+ position: relative;
43
+ display: flex;
44
+ flex-direction: column;
45
+ height: 100%;
46
+ border: 1px solid var(--sapGroup_ContentBorderColor, #d9d9d9);
47
+ border-radius: 0.5rem;
48
+ background: var(--sapBackgroundColor, #fff);
49
+ overflow: visible;
50
+
51
+ &__header {
52
+ padding: 0.75rem 1rem;
53
+ font-weight: 600;
54
+ border-bottom: 1px solid var(--sapGroup_ContentBorderColor, #d9d9d9);
55
+ background: var(--sapGroup_TitleBackground, #f5f5f5);
56
+ }
57
+
58
+ &__body {
59
+ flex: 1;
60
+ padding: 1rem;
61
+ overflow: auto;
62
+ }
63
+ }