@jskit-ai/users-web 0.1.80 → 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 (38) hide show
  1. package/package.descriptor.mjs +137 -21
  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/components/UsersHomeToolsWidget.vue +0 -1
  12. package/src/client/composables/records/useAddEdit.js +23 -2
  13. package/src/client/composables/records/useCrudList.js +5 -1
  14. package/src/client/composables/records/useView.js +1 -0
  15. package/src/client/composables/runtime/operationUiHelpers.js +7 -3
  16. package/src/client/composables/runtime/useEndpointResource.js +12 -2
  17. package/src/client/composables/runtime/useUiFeedback.js +4 -2
  18. package/src/client/composables/support/resourceLoadStateHelpers.js +33 -1
  19. package/src/client/composables/useAccountSettingsRuntime.js +10 -1
  20. package/src/client/composables/useCrudAddEditScreen.js +88 -0
  21. package/src/client/composables/useCrudListBulkActions.js +147 -0
  22. package/src/client/composables/useCrudListScreen.js +107 -0
  23. package/src/client/composables/useCrudViewScreen.js +67 -0
  24. package/src/client/composables/usePagedCollection.js +6 -1
  25. package/src/client/filters.js +15 -0
  26. package/src/client/index.js +5 -0
  27. package/src/shared/toolsOutletContracts.js +0 -4
  28. package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +34 -8
  29. package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +34 -8
  30. package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +34 -8
  31. package/test/crudListBulkActionSurface.test.js +27 -0
  32. package/test/crudListFilterSurface.test.js +45 -0
  33. package/test/crudScreenComponents.test.js +62 -0
  34. package/test/errorIntentContract.test.js +31 -0
  35. package/test/exportsContract.test.js +11 -0
  36. package/test/resourceLoadStateHelpers.test.js +35 -1
  37. package/test/settingsPlacementContract.test.js +146 -14
  38. package/test/useCrudListBulkActions.test.js +65 -0
@@ -3,7 +3,7 @@ import { HOME_COG_OUTLET } from "./src/shared/toolsOutletContracts.js";
3
3
  export default Object.freeze({
4
4
  packageVersion: 1,
5
5
  packageId: "@jskit-ai/users-web",
6
- version: "0.1.80",
6
+ version: "0.1.82",
7
7
  kind: "runtime",
8
8
  description: "Users web module: account/profile UI plus shared users web widgets.",
9
9
  dependsOn: [
@@ -50,6 +50,34 @@ export default Object.freeze({
50
50
  subpath: "./client/components/AccountSettingsClientElement",
51
51
  summary: "Exports the package-owned account settings host that renders placement-backed account sections."
52
52
  },
53
+ {
54
+ subpath: "./client/components/CrudAddEditScreen",
55
+ summary: "Exports the package-owned CRUD add/edit screen shell used by generated form pages."
56
+ },
57
+ {
58
+ subpath: "./client/components/CrudListBulkActionSurface",
59
+ summary: "Exports the adaptive CRUD list bulk-action surface for generated list pages."
60
+ },
61
+ {
62
+ subpath: "./client/components/CrudListFilterSurface",
63
+ summary: "Exports the adaptive CRUD list filter surface for generated list pages."
64
+ },
65
+ {
66
+ subpath: "./client/components/CrudListScreen",
67
+ summary: "Exports the package-owned CRUD list screen shell used by generated list pages."
68
+ },
69
+ {
70
+ subpath: "./client/components/CrudViewScreen",
71
+ summary: "Exports the package-owned CRUD view screen shell used by generated detail pages."
72
+ },
73
+ {
74
+ subpath: "./client/bulkActions",
75
+ summary: "Exports client-side CRUD list bulk-action definition helpers."
76
+ },
77
+ {
78
+ subpath: "./client/filters",
79
+ summary: "Exports client-side CRUD list filter definition helpers."
80
+ },
53
81
  {
54
82
  subpath: "./client/components/ProfileClientElement",
55
83
  summary: "Exports profile settings client element scaffold component."
@@ -78,10 +106,26 @@ export default Object.freeze({
78
106
  subpath: "./client/composables/useCrudListFilterLookups",
79
107
  summary: "Exports lookup-backed CRUD list filter helper for remote autocomplete filters."
80
108
  },
109
+ {
110
+ subpath: "./client/composables/useCrudAddEditScreen",
111
+ summary: "Exports the package-owned add/edit screen runtime for generated form pages."
112
+ },
113
+ {
114
+ subpath: "./client/composables/useCrudListBulkActions",
115
+ summary: "Exports selected-record state and execution runtime for generated CRUD list bulk actions."
116
+ },
117
+ {
118
+ subpath: "./client/composables/useCrudListScreen",
119
+ summary: "Exports the package-owned list screen runtime for generated list pages."
120
+ },
81
121
  {
82
122
  subpath: "./client/composables/useView",
83
123
  summary: "Exports read/view operation composable."
84
124
  },
125
+ {
126
+ subpath: "./client/composables/useCrudViewScreen",
127
+ summary: "Exports the package-owned view screen runtime for generated detail pages."
128
+ },
85
129
  {
86
130
  subpath: "./client/composables/usePagedCollection",
87
131
  summary: "Exports paged collection query composable."
@@ -112,7 +156,6 @@ export default Object.freeze({
112
156
  outlets: [
113
157
  {
114
158
  target: HOME_COG_OUTLET.target,
115
- defaultLinkComponentToken: HOME_COG_OUTLET.defaultLinkComponentToken,
116
159
  surfaces: ["home"],
117
160
  source: "src/client/components/UsersHomeToolsWidget.vue"
118
161
  },
@@ -122,19 +165,66 @@ export default Object.freeze({
122
165
  source: "src/client/components/AccountSettingsClientElement.vue"
123
166
  }
124
167
  ],
168
+ topology: {
169
+ placements: [
170
+ {
171
+ id: "home.tools-menu",
172
+ description: "Home surface tools menu actions.",
173
+ surfaces: ["home"],
174
+ variants: {
175
+ compact: {
176
+ outlet: HOME_COG_OUTLET.target,
177
+ renderers: {
178
+ link: "local.main.ui.surface-aware-menu-link-item"
179
+ }
180
+ },
181
+ medium: {
182
+ outlet: HOME_COG_OUTLET.target,
183
+ renderers: {
184
+ link: "local.main.ui.surface-aware-menu-link-item"
185
+ }
186
+ },
187
+ expanded: {
188
+ outlet: HOME_COG_OUTLET.target,
189
+ renderers: {
190
+ link: "local.main.ui.surface-aware-menu-link-item"
191
+ }
192
+ }
193
+ }
194
+ },
195
+ {
196
+ id: "settings.sections",
197
+ owner: "account-settings",
198
+ description: "Account settings content sections.",
199
+ surfaces: ["account"],
200
+ variants: {
201
+ compact: {
202
+ outlet: "account-settings:sections"
203
+ },
204
+ medium: {
205
+ outlet: "account-settings:sections"
206
+ },
207
+ expanded: {
208
+ outlet: "account-settings:sections"
209
+ }
210
+ }
211
+ }
212
+ ]
213
+ },
125
214
  contributions: [
126
215
  {
127
216
  id: "users.profile.menu.settings",
128
- target: "auth-profile-menu:primary-menu",
217
+ target: "auth.profile-menu",
218
+ kind: "link",
129
219
  surfaces: ["*"],
130
220
  order: 500,
131
- componentToken: "auth.web.profile.menu.link-item",
132
221
  when: "auth.authenticated === true",
133
222
  source: "mutations.text#users-web-profile-settings-placement"
134
223
  },
135
224
  {
136
225
  id: "users.home.tools.widget",
137
- target: "shell-layout:top-right",
226
+ target: "shell.status",
227
+ kind: "component",
138
228
  surfaces: ["home"],
139
229
  order: 900,
140
230
  componentToken: "users.web.home.tools.widget",
@@ -143,16 +233,18 @@ export default Object.freeze({
143
233
  },
144
234
  {
145
235
  id: "users.home.menu.settings",
146
- target: "home-cog:primary-menu",
236
+ target: "home.tools-menu",
237
+ kind: "link",
147
238
  surfaces: ["home"],
148
239
  order: 100,
149
- componentToken: "local.main.ui.surface-aware-menu-link-item",
150
240
  when: "auth.authenticated === true",
151
241
  source: "mutations.text#users-web-home-tools-placement"
152
242
  },
153
243
  {
154
244
  id: "users.account.settings.profile",
155
- target: "account-settings:sections",
245
+ target: "settings.sections",
246
+ owner: "account-settings",
247
+ kind: "component",
156
248
  surfaces: ["account"],
157
249
  order: 100,
158
250
  componentToken: "local.main.account-settings.section.profile",
@@ -160,7 +252,9 @@ export default Object.freeze({
160
252
  },
161
253
  {
162
254
  id: "users.account.settings.preferences",
163
- target: "account-settings:sections",
255
+ target: "settings.sections",
256
+ owner: "account-settings",
257
+ kind: "component",
164
258
  surfaces: ["account"],
165
259
  order: 200,
166
260
  componentToken: "local.main.account-settings.section.preferences",
@@ -168,7 +262,9 @@ export default Object.freeze({
168
262
  },
169
263
  {
170
264
  id: "users.account.settings.notifications",
171
- target: "account-settings:sections",
265
+ target: "settings.sections",
266
+ owner: "account-settings",
267
+ kind: "component",
172
268
  surfaces: ["account"],
173
269
  order: 300,
174
270
  componentToken: "local.main.account-settings.section.notifications",
@@ -181,15 +277,13 @@ export default Object.freeze({
181
277
  mutations: {
182
278
  dependencies: {
183
279
  runtime: {
184
- "@tanstack/vue-query": "5.92.12",
185
280
  "@mdi/js": "^7.4.47",
186
- "@jskit-ai/http-runtime": "0.1.64",
187
- "@jskit-ai/realtime": "0.1.64",
188
- "@jskit-ai/kernel": "0.1.65",
189
- "@jskit-ai/shell-web": "0.1.64",
190
- "@jskit-ai/uploads-image-web": "0.1.43",
191
- "@jskit-ai/users-core": "0.1.75",
192
- vuetify: "^4.0.0"
281
+ "@jskit-ai/http-runtime": "0.1.66",
282
+ "@jskit-ai/realtime": "0.1.66",
283
+ "@jskit-ai/kernel": "0.1.67",
284
+ "@jskit-ai/shell-web": "0.1.66",
285
+ "@jskit-ai/uploads-image-web": "0.1.45",
286
+ "@jskit-ai/users-core": "0.1.77"
193
287
  },
194
288
  dev: {}
195
289
  },
@@ -249,7 +343,7 @@ export default Object.freeze({
249
343
  position: "bottom",
250
344
  skipIfContains: "id: \"users.profile.menu.settings\"",
251
345
  value:
252
- "\naddPlacement({\n id: \"users.profile.menu.settings\",\n target: \"auth-profile-menu:primary-menu\",\n surfaces: [\"*\"],\n order: 500,\n componentToken: \"auth.web.profile.menu.link-item\",\n props: {\n label: \"Settings\",\n to: \"/account\"\n },\n when: ({ auth }) => auth?.authenticated === true\n});\n",
346
+ "\naddPlacement({\n id: \"users.profile.menu.settings\",\n target: \"auth.profile-menu\",\n kind: \"link\",\n surfaces: [\"*\"],\n order: 500,\n props: {\n label: \"Settings\",\n to: \"/account\"\n },\n when: ({ auth }) => auth?.authenticated === true\n});\n",
253
347
  reason: "Append users-web profile settings menu placement into app-owned placement registry.",
254
348
  category: "users-web",
255
349
  id: "users-web-profile-settings-placement"
@@ -260,22 +354,44 @@ export default Object.freeze({
260
354
  position: "bottom",
261
355
  skipIfContains: "id: \"users.home.tools.widget\"",
262
356
  value:
263
- "\naddPlacement({\n id: \"users.home.tools.widget\",\n target: \"shell-layout:top-right\",\n surfaces: [\"home\"],\n order: 900,\n componentToken: \"users.web.home.tools.widget\",\n when: ({ auth }) => auth?.authenticated === true\n});\n\naddPlacement({\n id: \"users.home.menu.settings\",\n target: \"home-cog:primary-menu\",\n surfaces: [\"home\"],\n order: 100,\n componentToken: \"local.main.ui.surface-aware-menu-link-item\",\n props: {\n label: \"Settings\",\n surface: \"home\",\n scopedSuffix: \"/settings\",\n unscopedSuffix: \"/settings\"\n },\n when: ({ auth }) => auth?.authenticated === true\n});\n",
357
+ "\naddPlacement({\n id: \"users.home.tools.widget\",\n target: \"shell.status\",\n kind: \"component\",\n surfaces: [\"home\"],\n order: 900,\n componentToken: \"users.web.home.tools.widget\",\n when: ({ auth }) => auth?.authenticated === true\n});\n\naddPlacement({\n id: \"users.home.menu.settings\",\n target: \"home.tools-menu\",\n kind: \"link\",\n surfaces: [\"home\"],\n order: 100,\n props: {\n label: \"Settings\",\n surface: \"home\",\n scopedSuffix: \"/settings\",\n unscopedSuffix: \"/settings\"\n },\n when: ({ auth }) => auth?.authenticated === true\n});\n",
264
358
  reason: "Append users-web home tools widget and settings menu placements into app-owned placement registry.",
265
359
  category: "users-web",
266
360
  id: "users-web-home-tools-placement"
267
361
  },
362
+ {
363
+ op: "append-text",
364
+ file: "src/placementTopology.js",
365
+ position: "bottom",
366
+ skipIfContains: "id: \"home.tools-menu\"",
367
+ value:
368
+ "\naddPlacementTopology({\n id: \"home.tools-menu\",\n description: \"Home surface tools menu actions.\",\n surfaces: [\"home\"],\n variants: {\n compact: {\n outlet: \"home-cog:primary-menu\",\n renderers: {\n link: \"local.main.ui.surface-aware-menu-link-item\"\n }\n },\n medium: {\n outlet: \"home-cog:primary-menu\",\n renderers: {\n link: \"local.main.ui.surface-aware-menu-link-item\"\n }\n },\n expanded: {\n outlet: \"home-cog:primary-menu\",\n renderers: {\n link: \"local.main.ui.surface-aware-menu-link-item\"\n }\n }\n }\n});\n",
369
+ reason: "Append users-web home tools semantic topology into app-owned placement topology.",
370
+ category: "users-web",
371
+ id: "users-web-home-tools-topology"
372
+ },
268
373
  {
269
374
  op: "append-text",
270
375
  file: "src/placement.js",
271
376
  position: "bottom",
272
377
  skipIfContains: "id: \"users.account.settings.profile\"",
273
378
  value:
274
- "\naddPlacement({\n id: \"users.account.settings.profile\",\n target: \"account-settings:sections\",\n surfaces: [\"account\"],\n order: 100,\n componentToken: \"local.main.account-settings.section.profile\",\n props: {\n title: \"Profile\",\n value: \"profile\",\n usesSharedRuntime: true\n }\n});\n\naddPlacement({\n id: \"users.account.settings.preferences\",\n target: \"account-settings:sections\",\n surfaces: [\"account\"],\n order: 200,\n componentToken: \"local.main.account-settings.section.preferences\",\n props: {\n title: \"Preferences\",\n value: \"preferences\",\n usesSharedRuntime: true\n }\n});\n\naddPlacement({\n id: \"users.account.settings.notifications\",\n target: \"account-settings:sections\",\n surfaces: [\"account\"],\n order: 300,\n componentToken: \"local.main.account-settings.section.notifications\",\n props: {\n title: \"Notifications\",\n value: \"notifications\",\n usesSharedRuntime: true\n }\n});\n",
379
+ "\naddPlacement({\n id: \"users.account.settings.profile\",\n target: \"settings.sections\",\n owner: \"account-settings\",\n kind: \"component\",\n surfaces: [\"account\"],\n order: 100,\n componentToken: \"local.main.account-settings.section.profile\",\n props: {\n title: \"Profile\",\n value: \"profile\",\n usesSharedRuntime: true\n }\n});\n\naddPlacement({\n id: \"users.account.settings.preferences\",\n target: \"settings.sections\",\n owner: \"account-settings\",\n kind: \"component\",\n surfaces: [\"account\"],\n order: 200,\n componentToken: \"local.main.account-settings.section.preferences\",\n props: {\n title: \"Preferences\",\n value: \"preferences\",\n usesSharedRuntime: true\n }\n});\n\naddPlacement({\n id: \"users.account.settings.notifications\",\n target: \"settings.sections\",\n owner: \"account-settings\",\n kind: \"component\",\n surfaces: [\"account\"],\n order: 300,\n componentToken: \"local.main.account-settings.section.notifications\",\n props: {\n title: \"Notifications\",\n value: \"notifications\",\n usesSharedRuntime: true\n }\n});\n",
275
380
  reason: "Append users-web account settings section placements into the app-owned placement registry.",
276
381
  category: "users-web",
277
382
  id: "users-web-account-settings-sections-placement"
278
383
  },
384
+ {
385
+ op: "append-text",
386
+ file: "src/placementTopology.js",
387
+ position: "bottom",
388
+ skipIfContains: "id: \"settings.sections\"",
389
+ value:
390
+ "\naddPlacementTopology({\n id: \"settings.sections\",\n owner: \"account-settings\",\n description: \"Account settings content sections.\",\n surfaces: [\"account\"],\n variants: {\n compact: {\n outlet: \"account-settings:sections\"\n },\n medium: {\n outlet: \"account-settings:sections\"\n },\n expanded: {\n outlet: \"account-settings:sections\"\n }\n }\n});\n",
391
+ reason: "Append users-web account settings semantic topology into app-owned placement topology.",
392
+ category: "users-web",
393
+ id: "users-web-account-settings-topology"
394
+ },
279
395
  {
280
396
  op: "append-text",
281
397
  file: "packages/main/src/client/providers/MainClientProvider.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.1.80",
3
+ "version": "0.1.82",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -8,22 +8,33 @@
8
8
  "exports": {
9
9
  "./client": "./src/client/index.js",
10
10
  "./client/components/AccountSettingsClientElement": "./src/client/components/AccountSettingsClientElement.vue",
11
+ "./client/components/CrudAddEditScreen": "./src/client/components/CrudAddEditScreen.vue",
12
+ "./client/components/CrudListBulkActionSurface": "./src/client/components/CrudListBulkActionSurface.vue",
13
+ "./client/components/CrudListFilterSurface": "./src/client/components/CrudListFilterSurface.vue",
14
+ "./client/components/CrudListScreen": "./src/client/components/CrudListScreen.vue",
15
+ "./client/components/CrudViewScreen": "./src/client/components/CrudViewScreen.vue",
11
16
  "./client/account-settings/sections": "./src/client/account-settings/sections.js",
17
+ "./client/bulkActions": "./src/client/bulkActions.js",
18
+ "./client/filters": "./src/client/filters.js",
12
19
  "./client/composables/useAddEdit": "./src/client/composables/records/useAddEdit.js",
13
20
  "./client/composables/useAccess": "./src/client/composables/useAccess.js",
14
21
  "./client/composables/useCommand": "./src/client/composables/useCommand.js",
15
22
  "./client/composables/useCrudAddEdit": "./src/client/composables/records/useCrudAddEdit.js",
23
+ "./client/composables/useCrudAddEditScreen": "./src/client/composables/useCrudAddEditScreen.js",
24
+ "./client/composables/useCrudListBulkActions": "./src/client/composables/useCrudListBulkActions.js",
16
25
  "./client/composables/crudLookupFieldRuntime": "./src/client/composables/crud/crudLookupFieldRuntime.js",
17
26
  "./client/composables/useCrudListFilterLookups": "./src/client/composables/useCrudListFilterLookups.js",
18
27
  "./client/composables/useCrudListFilters": "./src/client/composables/useCrudListFilters.js",
19
28
  "./client/composables/useEndpointResource": "./src/client/composables/runtime/useEndpointResource.js",
20
29
  "./client/composables/useList": "./src/client/composables/records/useList.js",
21
30
  "./client/composables/useCrudList": "./src/client/composables/records/useCrudList.js",
31
+ "./client/composables/useCrudListScreen": "./src/client/composables/useCrudListScreen.js",
22
32
  "./client/composables/useCrudListParentTitle": "./src/client/composables/useCrudListParentTitle.js",
23
33
  "./client/composables/useRealtimeQueryInvalidation": "./src/client/composables/useRealtimeQueryInvalidation.js",
24
34
  "./client/composables/useSurfaceRouteContext": "./src/client/composables/useSurfaceRouteContext.js",
25
35
  "./client/composables/useView": "./src/client/composables/records/useView.js",
26
36
  "./client/composables/useCrudView": "./src/client/composables/records/useCrudView.js",
37
+ "./client/composables/useCrudViewScreen": "./src/client/composables/useCrudViewScreen.js",
27
38
  "./client/composables/usePagedCollection": "./src/client/composables/usePagedCollection.js",
28
39
  "./client/composables/usePaths": "./src/client/composables/usePaths.js",
29
40
  "./client/composables/runtime/useUiFeedback": "./src/client/composables/runtime/useUiFeedback.js",
@@ -32,18 +43,18 @@
32
43
  "./client/support/contractGuards": "./src/client/support/contractGuards.js"
33
44
  },
34
45
  "dependencies": {
35
- "@tanstack/vue-query": "5.92.12",
36
46
  "@mdi/js": "^7.4.47",
37
- "@jskit-ai/http-runtime": "0.1.64",
38
- "@jskit-ai/kernel": "0.1.65",
39
- "@jskit-ai/realtime": "0.1.64",
40
- "@jskit-ai/shell-web": "0.1.64",
41
- "@jskit-ai/uploads-image-web": "0.1.43",
42
- "@jskit-ai/users-core": "0.1.75",
43
- "vuetify": "^4.0.0"
47
+ "@jskit-ai/http-runtime": "0.1.66",
48
+ "@jskit-ai/kernel": "0.1.67",
49
+ "@jskit-ai/realtime": "0.1.66",
50
+ "@jskit-ai/shell-web": "0.1.66",
51
+ "@jskit-ai/uploads-image-web": "0.1.45",
52
+ "@jskit-ai/users-core": "0.1.77"
44
53
  },
45
54
  "peerDependencies": {
55
+ "@tanstack/vue-query": "^5.90.5",
46
56
  "vue": "^3.5.13",
47
- "vue-router": "^5.0.4"
57
+ "vue-router": "^5.0.4",
58
+ "vuetify": "^4.0.0"
48
59
  }
49
60
  }
@@ -0,0 +1,47 @@
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+
3
+ function normalizeCrudListBulkAction(rawAction = {}, index = 0) {
4
+ if (!rawAction || typeof rawAction !== "object" || Array.isArray(rawAction)) {
5
+ return null;
6
+ }
7
+
8
+ const key = normalizeText(rawAction.key || rawAction.id || `action-${index + 1}`);
9
+ const label = normalizeText(rawAction.label || rawAction.title);
10
+ if (!key || !label) {
11
+ return null;
12
+ }
13
+
14
+ return Object.freeze({
15
+ key,
16
+ label,
17
+ icon: normalizeText(rawAction.icon),
18
+ color: normalizeText(rawAction.color, { fallback: "primary" }),
19
+ variant: normalizeText(rawAction.variant, { fallback: "tonal" }),
20
+ confirmLabel: normalizeText(rawAction.confirmLabel),
21
+ run: typeof rawAction.run === "function" ? rawAction.run : null,
22
+ disabled: rawAction.disabled
23
+ });
24
+ }
25
+
26
+ function defineCrudListBulkActions(actions = []) {
27
+ if (!Array.isArray(actions)) {
28
+ throw new TypeError("defineCrudListBulkActions requires an array.");
29
+ }
30
+
31
+ const normalizedActions = [];
32
+ const seenKeys = new Set();
33
+
34
+ actions.forEach((rawAction, index) => {
35
+ const action = normalizeCrudListBulkAction(rawAction, index);
36
+ if (!action || seenKeys.has(action.key)) {
37
+ return;
38
+ }
39
+
40
+ seenKeys.add(action.key);
41
+ normalizedActions.push(action);
42
+ });
43
+
44
+ return Object.freeze(normalizedActions);
45
+ }
46
+
47
+ export { defineCrudListBulkActions };
@@ -56,28 +56,41 @@ const activeTab = computed({
56
56
 
57
57
  <template>
58
58
  <section class="settings-view py-2 py-md-4">
59
- <v-card class="panel-card" rounded="lg" elevation="1" border>
60
- <v-card-item>
61
- <v-card-title class="panel-title">Account settings</v-card-title>
62
- <v-card-subtitle>Global profile, preferences, notifications, and account controls.</v-card-subtitle>
63
- <template #append>
64
- <v-btn
65
- variant="text"
66
- color="secondary"
67
- :to="runtime.backNavigationTarget.value.sameOrigin ? runtime.backNavigationTarget.value.href : undefined"
68
- :href="runtime.backNavigationTarget.value.sameOrigin ? undefined : runtime.backNavigationTarget.value.href"
69
- >
70
- Back
71
- </v-btn>
72
- </template>
73
- </v-card-item>
74
- <v-divider />
59
+ <v-sheet class="settings-panel" rounded="lg" border>
60
+ <header class="settings-panel__header">
61
+ <div>
62
+ <h1 class="settings-panel__title">Account settings</h1>
63
+ <p class="text-body-2 text-medium-emphasis mb-0">
64
+ Global profile, preferences, notifications, and account controls.
65
+ </p>
66
+ </div>
67
+ <v-btn
68
+ variant="text"
69
+ color="secondary"
70
+ :to="runtime.backNavigationTarget.value.sameOrigin ? runtime.backNavigationTarget.value.href : undefined"
71
+ :href="runtime.backNavigationTarget.value.sameOrigin ? undefined : runtime.backNavigationTarget.value.href"
72
+ >
73
+ Back
74
+ </v-btn>
75
+ </header>
75
76
 
76
- <v-card-text class="pt-4">
77
+ <div class="settings-panel__body">
77
78
  <template v-if="runtime.loadingSettings.value">
78
79
  <v-skeleton-loader type="text@2, list-item-two-line@4" class="mb-4" />
79
80
  <v-skeleton-loader type="text@2, paragraph, button" />
80
81
  </template>
82
+ <div v-else-if="runtime.settingsLoadError.value" class="settings-panel__state">
83
+ <h2 class="text-h6 mb-2">Unable to load account settings</h2>
84
+ <p class="text-body-2 text-medium-emphasis mb-4">{{ runtime.settingsLoadError.value }}</p>
85
+ <v-btn
86
+ color="primary"
87
+ variant="tonal"
88
+ :loading="runtime.refreshingSettings.value"
89
+ @click="runtime.refreshSettings"
90
+ >
91
+ Retry
92
+ </v-btn>
93
+ </div>
81
94
  <template v-else-if="sections.length < 1">
82
95
  <p class="text-body-2 text-medium-emphasis mb-0">No account settings sections are registered.</p>
83
96
  </template>
@@ -109,20 +122,42 @@ const activeTab = computed({
109
122
  </v-col>
110
123
  </v-row>
111
124
  </template>
112
- </v-card-text>
113
- </v-card>
125
+ </div>
126
+ </v-sheet>
114
127
  </section>
115
128
  </template>
116
129
 
117
130
  <style scoped>
118
- .panel-card {
131
+ .settings-panel {
119
132
  background-color: rgb(var(--v-theme-surface));
133
+ overflow: hidden;
134
+ }
135
+
136
+ .settings-panel__header {
137
+ align-items: flex-start;
138
+ display: flex;
139
+ gap: 1rem;
140
+ justify-content: space-between;
141
+ padding: 1rem 1rem 0;
120
142
  }
121
143
 
122
- .panel-title {
123
- font-size: 1rem;
124
- font-weight: 600;
125
- letter-spacing: 0.01em;
144
+ .settings-panel__title {
145
+ font-size: clamp(1.35rem, 2vw, 1.85rem);
146
+ font-weight: 650;
147
+ letter-spacing: -0.02em;
148
+ line-height: 1.15;
149
+ margin: 0 0 0.35rem;
150
+ }
151
+
152
+ .settings-panel__body {
153
+ padding: 1rem;
154
+ }
155
+
156
+ .settings-panel__state {
157
+ margin-inline: auto;
158
+ max-width: 30rem;
159
+ padding: 2rem 1rem;
160
+ text-align: center;
126
161
  }
127
162
 
128
163
  .settings-section-list {
@@ -139,4 +174,14 @@ const activeTab = computed({
139
174
  :deep(.settings-sections-window .v-window-x-reverse-transition-leave-active) {
140
175
  transition: none !important;
141
176
  }
177
+
178
+ @media (max-width: 960px) {
179
+ .settings-panel__header {
180
+ flex-direction: column;
181
+ }
182
+
183
+ .settings-panel__header :deep(.v-btn) {
184
+ min-height: 48px;
185
+ }
186
+ }
142
187
  </style>