@jskit-ai/users-web 0.1.81 → 0.1.82

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 (36) hide show
  1. package/package.descriptor.mjs +51 -9
  2. package/package.json +21 -10
  3. package/src/client/bulkActions.js +47 -0
  4. package/src/client/components/AccountSettingsClientElement.vue +69 -24
  5. package/src/client/components/CrudAddEditScreen.vue +186 -0
  6. package/src/client/components/CrudListBulkActionSurface.vue +126 -0
  7. package/src/client/components/CrudListFilterSurface.vue +377 -0
  8. package/src/client/components/CrudListScreen.vue +434 -0
  9. package/src/client/components/CrudViewScreen.vue +186 -0
  10. package/src/client/components/ProfileClientElement.vue +19 -12
  11. package/src/client/composables/records/useAddEdit.js +23 -2
  12. package/src/client/composables/records/useCrudList.js +5 -1
  13. package/src/client/composables/records/useView.js +1 -0
  14. package/src/client/composables/runtime/operationUiHelpers.js +7 -3
  15. package/src/client/composables/runtime/useEndpointResource.js +12 -2
  16. package/src/client/composables/runtime/useUiFeedback.js +4 -2
  17. package/src/client/composables/support/resourceLoadStateHelpers.js +33 -1
  18. package/src/client/composables/useAccountSettingsRuntime.js +10 -1
  19. package/src/client/composables/useCrudAddEditScreen.js +88 -0
  20. package/src/client/composables/useCrudListBulkActions.js +147 -0
  21. package/src/client/composables/useCrudListScreen.js +107 -0
  22. package/src/client/composables/useCrudViewScreen.js +67 -0
  23. package/src/client/composables/usePagedCollection.js +6 -1
  24. package/src/client/filters.js +15 -0
  25. package/src/client/index.js +5 -0
  26. package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +34 -8
  27. package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +34 -8
  28. package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +34 -8
  29. package/test/crudListBulkActionSurface.test.js +27 -0
  30. package/test/crudListFilterSurface.test.js +45 -0
  31. package/test/crudScreenComponents.test.js +62 -0
  32. package/test/errorIntentContract.test.js +31 -0
  33. package/test/exportsContract.test.js +11 -0
  34. package/test/resourceLoadStateHelpers.test.js +35 -1
  35. package/test/settingsPlacementContract.test.js +61 -0
  36. package/test/useCrudListBulkActions.test.js +65 -0
@@ -0,0 +1,434 @@
1
+ <script setup>
2
+ import { computed, unref } from "vue";
3
+ import { useRoute } from "vue-router";
4
+ import CrudListBulkActionSurface from "./CrudListBulkActionSurface.vue";
5
+ import CrudListFilterSurface from "./CrudListFilterSurface.vue";
6
+
7
+ const props = defineProps({
8
+ screen: {
9
+ type: Object,
10
+ required: true
11
+ },
12
+ titleLabel: {
13
+ type: String,
14
+ default: "Records"
15
+ },
16
+ headingTitle: {
17
+ type: String,
18
+ default: ""
19
+ },
20
+ subtitle: {
21
+ type: String,
22
+ default: ""
23
+ },
24
+ createLabel: {
25
+ type: String,
26
+ default: "New record"
27
+ },
28
+ loadErrorTitle: {
29
+ type: String,
30
+ default: "Unable to load records"
31
+ },
32
+ loadErrorBody: {
33
+ type: String,
34
+ default: "Check the connection and try again."
35
+ },
36
+ emptyTitle: {
37
+ type: String,
38
+ default: "No records yet"
39
+ },
40
+ emptyBody: {
41
+ type: String,
42
+ default: "Create the first record to start using this workflow."
43
+ }
44
+ });
45
+
46
+ const route = useRoute();
47
+ const records = computed(() => props.screen?.records || {});
48
+ const bulkActions = computed(() => props.screen?.bulkActions || {});
49
+ const listFilters = computed(() => props.screen?.listFilters || {});
50
+ const filterRuntime = computed(() => props.screen?.filterRuntime || null);
51
+ const listPrimaryAction = computed(() => unref(props.screen?.listPrimaryAction) || "");
52
+ const hasBulkActions = computed(() => Boolean(unref(bulkActions.value?.hasActions)));
53
+ const hasViewUrl = computed(() => Boolean(props.screen?.hasViewUrl));
54
+ const hasEditUrl = computed(() => Boolean(props.screen?.hasEditUrl));
55
+ const resolvedHeadingTitle = computed(() => String(props.headingTitle || props.titleLabel || "").trim());
56
+ const resolvedSubtitle = computed(() =>
57
+ String(props.subtitle || `Search, review, and update ${props.titleLabel} from this screen.`).trim()
58
+ );
59
+
60
+ function resolveListRecordTitle(record) {
61
+ if (typeof props.screen?.resolveRecordTitle === "function") {
62
+ return props.screen.resolveRecordTitle(record);
63
+ }
64
+ return "Record";
65
+ }
66
+
67
+ function formatListCardValue(value) {
68
+ if (typeof props.screen?.formatListCardValue === "function") {
69
+ return props.screen.formatListCardValue(value);
70
+ }
71
+ return value;
72
+ }
73
+
74
+ function resolveViewLocation(record) {
75
+ const path = typeof records.value?.resolveViewUrl === "function"
76
+ ? records.value.resolveViewUrl(record)
77
+ : "";
78
+ return path ? { path, query: route.query } : null;
79
+ }
80
+
81
+ function resolveEditLocation(record) {
82
+ const path = typeof records.value?.resolveEditUrl === "function"
83
+ ? records.value.resolveEditUrl(record)
84
+ : "";
85
+ return path ? { path, query: route.query } : null;
86
+ }
87
+ </script>
88
+
89
+ <template>
90
+ <section class="generated-ui-screen generated-ui-screen--operator ui-generator-list-element d-flex flex-column ga-4">
91
+ <header class="ui-generator-list-header">
92
+ <div class="ui-generator-list-header__copy">
93
+ <p class="text-overline text-medium-emphasis mb-1">{{ titleLabel }}</p>
94
+ <h1 class="ui-generator-list-header__title">{{ resolvedHeadingTitle }}</h1>
95
+ <p class="text-body-2 text-medium-emphasis mb-0">{{ resolvedSubtitle }}</p>
96
+ </div>
97
+ <div class="ui-generator-list-header__actions">
98
+ <v-btn color="primary" variant="tonal" :loading="records.isFetching" @click="records.reload">Refresh</v-btn>
99
+ <v-btn
100
+ v-if="listPrimaryAction"
101
+ class="ui-generator-list-header__primary-action"
102
+ color="primary"
103
+ variant="flat"
104
+ :to="listPrimaryAction"
105
+ >
106
+ {{ createLabel }}
107
+ </v-btn>
108
+ </div>
109
+ </header>
110
+
111
+ <v-sheet rounded="lg" border class="ui-generator-list-panel">
112
+ <div class="ui-generator-list-toolbar">
113
+ <v-text-field
114
+ v-if="records.searchEnabled"
115
+ v-model="records.searchQuery"
116
+ :label="records.searchLabel"
117
+ :placeholder="records.searchPlaceholder"
118
+ variant="outlined"
119
+ density="comfortable"
120
+ hide-details="auto"
121
+ clearable
122
+ class="ui-generator-list-search"
123
+ :loading="records.isSearchDebouncing"
124
+ />
125
+ <CrudListFilterSurface
126
+ :filters="listFilters"
127
+ :runtime="filterRuntime"
128
+ />
129
+ </div>
130
+ <CrudListBulkActionSurface :runtime="bulkActions" />
131
+
132
+ <template v-if="records.showListSkeleton">
133
+ <div class="pa-4">
134
+ <v-skeleton-loader type="text@2, list-item-two-line@5" />
135
+ </div>
136
+ </template>
137
+ <template v-else>
138
+ <v-progress-linear v-if="records.isRefetching" indeterminate />
139
+
140
+ <div v-if="records.loadError" class="ui-generator-list-state">
141
+ <h2 class="text-h6 mb-2">{{ loadErrorTitle }}</h2>
142
+ <p class="text-body-2 text-medium-emphasis mb-4">{{ loadErrorBody }}</p>
143
+ <v-btn color="primary" variant="tonal" :loading="records.isFetching" @click="records.reload">Retry</v-btn>
144
+ </div>
145
+
146
+ <div v-else-if="records.items.length < 1" class="ui-generator-list-state">
147
+ <h2 class="text-h6 mb-2">{{ emptyTitle }}</h2>
148
+ <p class="text-body-2 text-medium-emphasis mb-4">{{ emptyBody }}</p>
149
+ <v-btn v-if="listPrimaryAction" color="primary" variant="flat" :to="listPrimaryAction">
150
+ {{ createLabel }}
151
+ </v-btn>
152
+ </div>
153
+
154
+ <template v-else>
155
+ <div class="ui-generator-list-cards d-md-none">
156
+ <v-sheet
157
+ v-for="(record, index) in records.items"
158
+ :key="records.resolveRowKey(record, index)"
159
+ rounded="lg"
160
+ border
161
+ class="ui-generator-list-card"
162
+ >
163
+ <div class="ui-generator-list-card__header">
164
+ <v-checkbox-btn
165
+ v-if="hasBulkActions"
166
+ :model-value="bulkActions.isRecordSelected(record, index)"
167
+ :aria-label="`Select ${resolveListRecordTitle(record)}`"
168
+ class="ui-generator-list-card__select"
169
+ @update:model-value="bulkActions.setRecordSelected(record, index, $event)"
170
+ />
171
+ <div class="min-w-0">
172
+ <div class="ui-generator-list-card__title">{{ resolveListRecordTitle(record) }}</div>
173
+ <div class="text-caption text-medium-emphasis">
174
+ {{ records.resolveRowKey(record, index) }}
175
+ </div>
176
+ </div>
177
+ <v-menu v-if="hasViewUrl || hasEditUrl" location="bottom end">
178
+ <template #activator="{ props: menuProps }">
179
+ <v-btn v-bind="menuProps" variant="text" size="small">Actions</v-btn>
180
+ </template>
181
+ <v-list density="compact" min-width="140">
182
+ <v-list-item
183
+ v-if="hasViewUrl"
184
+ title="Open"
185
+ :to="resolveViewLocation(record)"
186
+ :disabled="!resolveViewLocation(record)"
187
+ />
188
+ <v-list-item
189
+ v-if="hasEditUrl"
190
+ title="Edit"
191
+ :to="resolveEditLocation(record)"
192
+ :disabled="!resolveEditLocation(record)"
193
+ />
194
+ </v-list>
195
+ </v-menu>
196
+ </div>
197
+ <div class="ui-generator-list-card__fields">
198
+ <slot
199
+ name="card-fields"
200
+ :record="record"
201
+ :records="records"
202
+ :index="index"
203
+ :format-list-card-value="formatListCardValue"
204
+ />
205
+ </div>
206
+ </v-sheet>
207
+ </div>
208
+
209
+ <div class="ui-generator-list-table d-none d-md-block">
210
+ <v-table density="comfortable">
211
+ <thead>
212
+ <tr>
213
+ <th v-if="hasBulkActions" class="ui-generator-list-table__select">
214
+ <v-checkbox-btn
215
+ :model-value="bulkActions.allVisibleSelected(records.items)"
216
+ :indeterminate="
217
+ bulkActions.someVisibleSelected(records.items) &&
218
+ !bulkActions.allVisibleSelected(records.items)
219
+ "
220
+ aria-label="Select visible rows"
221
+ @update:model-value="bulkActions.setVisibleSelected(records.items, $event)"
222
+ />
223
+ </th>
224
+ <slot name="table-header" />
225
+ <th v-if="hasViewUrl" class="text-right" />
226
+ <th v-if="hasEditUrl" class="text-right" />
227
+ </tr>
228
+ </thead>
229
+ <tbody>
230
+ <tr v-for="(record, index) in records.items" :key="records.resolveRowKey(record, index)">
231
+ <td v-if="hasBulkActions" class="ui-generator-list-table__select">
232
+ <v-checkbox-btn
233
+ :model-value="bulkActions.isRecordSelected(record, index)"
234
+ :aria-label="`Select ${resolveListRecordTitle(record)}`"
235
+ @update:model-value="bulkActions.setRecordSelected(record, index, $event)"
236
+ />
237
+ </td>
238
+ <slot name="table-row" :record="record" :records="records" :index="index" />
239
+ <td v-if="hasViewUrl" class="text-right">
240
+ <v-btn
241
+ size="small"
242
+ color="primary"
243
+ variant="outlined"
244
+ :to="resolveViewLocation(record)"
245
+ :disabled="!resolveViewLocation(record)"
246
+ >
247
+ Open
248
+ </v-btn>
249
+ </td>
250
+ <td v-if="hasEditUrl" class="text-right">
251
+ <v-btn
252
+ size="small"
253
+ color="primary"
254
+ variant="tonal"
255
+ :to="resolveEditLocation(record)"
256
+ :disabled="!resolveEditLocation(record)"
257
+ >
258
+ Edit
259
+ </v-btn>
260
+ </td>
261
+ </tr>
262
+ </tbody>
263
+ </v-table>
264
+ </div>
265
+ </template>
266
+
267
+ <div v-if="records.hasMore" class="d-flex justify-center pa-4">
268
+ <v-btn color="primary" variant="outlined" :loading="records.isLoadingMore" @click="records.loadMore">
269
+ Load more
270
+ </v-btn>
271
+ </div>
272
+ </template>
273
+ </v-sheet>
274
+
275
+ <v-btn
276
+ v-if="listPrimaryAction"
277
+ class="ui-generator-list-fab d-md-none"
278
+ color="primary"
279
+ variant="flat"
280
+ :to="listPrimaryAction"
281
+ >
282
+ New
283
+ </v-btn>
284
+ </section>
285
+ </template>
286
+
287
+ <style scoped>
288
+ .generated-ui-screen {
289
+ --generated-ui-screen-title-size: clamp(1.35rem, 2vw, 1.85rem);
290
+ --generated-ui-screen-state-padding: 2.5rem 1.25rem;
291
+ }
292
+
293
+ .generated-ui-screen--operator {
294
+ --generated-ui-screen-state-padding: 2rem 1rem;
295
+ }
296
+
297
+ .ui-generator-list-header {
298
+ align-items: flex-start;
299
+ display: flex;
300
+ gap: 1rem;
301
+ justify-content: space-between;
302
+ }
303
+
304
+ .ui-generator-list-header__copy {
305
+ min-width: 0;
306
+ }
307
+
308
+ .ui-generator-list-header__title {
309
+ font-size: var(--generated-ui-screen-title-size);
310
+ font-weight: 650;
311
+ letter-spacing: -0.02em;
312
+ line-height: 1.15;
313
+ margin: 0 0 0.35rem;
314
+ }
315
+
316
+ .ui-generator-list-header__actions {
317
+ display: flex;
318
+ flex-wrap: wrap;
319
+ gap: 0.5rem;
320
+ justify-content: flex-end;
321
+ }
322
+
323
+ .ui-generator-list-panel {
324
+ overflow: hidden;
325
+ }
326
+
327
+ .ui-generator-list-toolbar {
328
+ padding: 1rem;
329
+ }
330
+
331
+ .ui-generator-list-search {
332
+ max-width: 26rem;
333
+ }
334
+
335
+ .ui-generator-list-state {
336
+ margin-inline: auto;
337
+ max-width: 30rem;
338
+ padding: var(--generated-ui-screen-state-padding);
339
+ text-align: center;
340
+ }
341
+
342
+ .ui-generator-list-cards {
343
+ display: flex;
344
+ flex-direction: column;
345
+ gap: 0.75rem;
346
+ padding: 0 1rem 1rem;
347
+ }
348
+
349
+ .ui-generator-list-card {
350
+ padding: 0.875rem;
351
+ }
352
+
353
+ .ui-generator-list-card__header {
354
+ align-items: flex-start;
355
+ display: flex;
356
+ gap: 0.75rem;
357
+ justify-content: space-between;
358
+ }
359
+
360
+ .ui-generator-list-card__select {
361
+ flex: 0 0 auto;
362
+ margin-inline-start: -0.35rem;
363
+ margin-top: -0.35rem;
364
+ }
365
+
366
+ .ui-generator-list-card__title {
367
+ font-size: 1rem;
368
+ font-weight: 650;
369
+ line-height: 1.25;
370
+ overflow-wrap: anywhere;
371
+ }
372
+
373
+ .ui-generator-list-card__fields {
374
+ display: grid;
375
+ gap: 0.65rem;
376
+ margin-top: 0.85rem;
377
+ }
378
+
379
+ .ui-generator-list-card__field {
380
+ display: grid;
381
+ gap: 0.15rem;
382
+ }
383
+
384
+ .ui-generator-list-card__field-label {
385
+ color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
386
+ font-size: 0.72rem;
387
+ letter-spacing: 0.04em;
388
+ line-height: 1.2;
389
+ text-transform: uppercase;
390
+ }
391
+
392
+ .ui-generator-list-card__field-value {
393
+ font-size: 0.95rem;
394
+ line-height: 1.35;
395
+ overflow-wrap: anywhere;
396
+ }
397
+
398
+ .ui-generator-list-table {
399
+ overflow-x: auto;
400
+ }
401
+
402
+ .ui-generator-list-table__select {
403
+ width: 3rem;
404
+ }
405
+
406
+ .ui-generator-list-fab {
407
+ bottom: calc(5rem + env(safe-area-inset-bottom, 0px));
408
+ position: fixed;
409
+ right: 1rem;
410
+ z-index: 6;
411
+ }
412
+
413
+ @media (max-width: 960px) {
414
+ .ui-generator-list-header {
415
+ flex-direction: column;
416
+ }
417
+
418
+ .ui-generator-list-header__actions {
419
+ width: 100%;
420
+ }
421
+
422
+ .ui-generator-list-header__actions :deep(.v-btn) {
423
+ min-height: 48px;
424
+ }
425
+
426
+ .ui-generator-list-header__primary-action {
427
+ display: none;
428
+ }
429
+
430
+ .ui-generator-list-search {
431
+ max-width: none;
432
+ }
433
+ }
434
+ </style>
@@ -0,0 +1,186 @@
1
+ <script setup>
2
+ import { computed, unref } from "vue";
3
+
4
+ const props = defineProps({
5
+ screen: {
6
+ type: Object,
7
+ required: true
8
+ },
9
+ resourceSingularTitle: {
10
+ type: String,
11
+ default: "Record"
12
+ },
13
+ resourcePluralTitle: {
14
+ type: String,
15
+ default: "Records"
16
+ },
17
+ description: {
18
+ type: String,
19
+ default: ""
20
+ },
21
+ unavailableTitle: {
22
+ type: String,
23
+ default: "Record unavailable"
24
+ }
25
+ });
26
+
27
+ const view = computed(() => props.screen?.view || {});
28
+ const recordTitle = computed(() => unref(props.screen?.recordTitle) || props.resourceSingularTitle);
29
+ const listLocation = computed(() => unref(props.screen?.listLocation) || null);
30
+ const editLocation = computed(() => unref(props.screen?.editLocation) || null);
31
+ const resolvedDescription = computed(() =>
32
+ String(props.description || `Review this ${props.resourceSingularTitle} record.`).trim()
33
+ );
34
+ </script>
35
+
36
+ <template>
37
+ <section class="generated-ui-screen generated-ui-screen--operator ui-generator-view-element d-flex flex-column ga-4">
38
+ <header class="ui-generator-view-header">
39
+ <div class="ui-generator-view-header__copy">
40
+ <p class="text-overline text-medium-emphasis mb-1">{{ resourceSingularTitle }}</p>
41
+ <h1 class="ui-generator-view-header__title">{{ recordTitle }}</h1>
42
+ <p class="text-body-2 text-medium-emphasis mb-0">{{ resolvedDescription }}</p>
43
+ </div>
44
+ <div class="ui-generator-view-header__actions">
45
+ <v-btn
46
+ v-if="listLocation"
47
+ color="primary"
48
+ variant="outlined"
49
+ :to="listLocation"
50
+ >
51
+ Back to {{ resourcePluralTitle }}
52
+ </v-btn>
53
+ <v-btn
54
+ v-if="editLocation"
55
+ color="primary"
56
+ variant="flat"
57
+ :to="editLocation"
58
+ >
59
+ Edit
60
+ </v-btn>
61
+ </div>
62
+ </header>
63
+
64
+ <v-sheet rounded="lg" border class="ui-generator-view-panel">
65
+ <div v-if="view.loadError || view.isNotFound" class="ui-generator-view-state">
66
+ <h2 class="text-h6 mb-2">{{ unavailableTitle }}</h2>
67
+ <p class="text-body-2 text-medium-emphasis mb-4">
68
+ {{ view.loadError || `This ${resourceSingularTitle} could not be found.` }}
69
+ </p>
70
+ <div class="ui-generator-view-state__actions">
71
+ <v-btn
72
+ v-if="view.loadError"
73
+ color="primary"
74
+ variant="tonal"
75
+ :loading="view.isFetching"
76
+ @click="view.refresh"
77
+ >
78
+ Retry
79
+ </v-btn>
80
+ <v-btn
81
+ v-else-if="listLocation"
82
+ color="primary"
83
+ variant="tonal"
84
+ :to="listLocation"
85
+ >
86
+ Back to {{ resourcePluralTitle }}
87
+ </v-btn>
88
+ </div>
89
+ </div>
90
+
91
+ <template v-else-if="view.isLoading">
92
+ <div class="pa-4">
93
+ <v-skeleton-loader type="text@2, list-item-two-line@5" />
94
+ </div>
95
+ </template>
96
+
97
+ <template v-else>
98
+ <v-progress-linear v-if="view.isRefetching" indeterminate />
99
+ <div class="pa-4">
100
+ <v-row class="ui-generator-view-fields">
101
+ <slot name="fields" :view="view" />
102
+ </v-row>
103
+ </div>
104
+ </template>
105
+ </v-sheet>
106
+ </section>
107
+ </template>
108
+
109
+ <style scoped>
110
+ .generated-ui-screen {
111
+ --generated-ui-screen-title-size: clamp(1.35rem, 2vw, 1.85rem);
112
+ --generated-ui-screen-state-padding: 2.5rem 1.25rem;
113
+ }
114
+
115
+ .generated-ui-screen--operator {
116
+ --generated-ui-screen-state-padding: 2rem 1rem;
117
+ }
118
+
119
+ .ui-generator-view-header {
120
+ align-items: flex-start;
121
+ display: flex;
122
+ gap: 1rem;
123
+ justify-content: space-between;
124
+ }
125
+
126
+ .ui-generator-view-header__copy {
127
+ min-width: 0;
128
+ }
129
+
130
+ .ui-generator-view-header__title {
131
+ font-size: var(--generated-ui-screen-title-size);
132
+ font-weight: 650;
133
+ letter-spacing: -0.02em;
134
+ line-height: 1.15;
135
+ margin: 0 0 0.35rem;
136
+ overflow-wrap: anywhere;
137
+ }
138
+
139
+ .ui-generator-view-header__actions {
140
+ display: flex;
141
+ flex-wrap: wrap;
142
+ gap: 0.5rem;
143
+ justify-content: flex-end;
144
+ }
145
+
146
+ .ui-generator-view-panel {
147
+ overflow: hidden;
148
+ }
149
+
150
+ .ui-generator-view-state {
151
+ margin-inline: auto;
152
+ max-width: 30rem;
153
+ padding: var(--generated-ui-screen-state-padding);
154
+ text-align: center;
155
+ }
156
+
157
+ .ui-generator-view-state__actions {
158
+ display: flex;
159
+ flex-wrap: wrap;
160
+ gap: 0.5rem;
161
+ justify-content: center;
162
+ }
163
+
164
+ .ui-generator-view-fields :deep(.v-col) {
165
+ min-width: 0;
166
+ }
167
+
168
+ @media (max-width: 960px) {
169
+ .ui-generator-view-header {
170
+ flex-direction: column;
171
+ }
172
+
173
+ .ui-generator-view-header__actions {
174
+ width: 100%;
175
+ }
176
+
177
+ .ui-generator-view-header__actions :deep(.v-btn) {
178
+ min-height: 48px;
179
+ flex: 1 1 10rem;
180
+ }
181
+
182
+ .ui-generator-view-state__actions :deep(.v-btn) {
183
+ min-height: 48px;
184
+ }
185
+ }
186
+ </style>
@@ -1,19 +1,18 @@
1
1
  <template>
2
2
  <section :class="rootClasses" :data-testid="uiTestIds.root">
3
- <v-card
4
- class="profile-client-card"
3
+ <v-sheet
4
+ class="profile-client-panel"
5
5
  :class="uiClasses.card"
6
6
  :rounded="resolvedVariant.surface === 'plain' ? '0' : 'lg'"
7
7
  :elevation="0"
8
8
  :border="resolvedVariant.surface !== 'plain'"
9
- :variant="resolvedVariant.surface === 'plain' ? 'text' : undefined"
10
9
  :data-testid="uiTestIds.card"
11
10
  >
12
- <v-card-item v-if="resolvedFeatures.header">
13
- <v-card-title class="text-subtitle-1">{{ copyText.title }}</v-card-title>
14
- </v-card-item>
11
+ <div v-if="resolvedFeatures.header" class="profile-client-panel__header">
12
+ <h2 class="text-subtitle-1 mb-0">{{ copyText.title }}</h2>
13
+ </div>
15
14
  <v-divider v-if="resolvedFeatures.header" />
16
- <v-card-text>
15
+ <div class="profile-client-panel__body">
17
16
  <slot name="form-before" :state="state" :actions="actions" />
18
17
 
19
18
  <v-form @submit.prevent="onSubmitProfile" novalidate>
@@ -81,8 +80,8 @@
81
80
  </v-form>
82
81
 
83
82
  <slot name="footer-extra" :state="state" :actions="actions" />
84
- </v-card-text>
85
- </v-card>
83
+ </div>
84
+ </v-sheet>
86
85
  </section>
87
86
  </template>
88
87
 
@@ -222,12 +221,20 @@ async function onAvatarRemove() {
222
221
  </script>
223
222
 
224
223
  <style scoped>
225
- .profile-client-element--layout-compact :deep(.v-card-item),
226
- .profile-client-element--layout-compact :deep(.v-card-text) {
224
+ .profile-client-panel__header {
225
+ padding: 1rem 1rem 0.75rem;
226
+ }
227
+
228
+ .profile-client-panel__body {
229
+ padding: 1rem;
230
+ }
231
+
232
+ .profile-client-element--layout-compact .profile-client-panel__header,
233
+ .profile-client-element--layout-compact .profile-client-panel__body {
227
234
  padding-block: 0.72rem;
228
235
  }
229
236
 
230
- .profile-client-element--surface-plain .profile-client-card {
237
+ .profile-client-element--surface-plain .profile-client-panel {
231
238
  box-shadow: none;
232
239
  border-width: 0;
233
240
  }