@jskit-ai/users-web 0.1.19 → 0.1.20
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.
- package/package.descriptor.mjs +6 -6
- package/package.json +6 -6
- package/src/client/components/WorkspaceSettingsClientElement.vue +131 -33
- package/src/client/composables/useAccountSettingsRuntime.js +2 -0
- package/src/client/lib/theme.js +78 -8
- package/src/client/runtime/bootstrapPlacementRuntime.js +2 -0
- package/test/bootstrapPlacementRuntime.test.js +41 -7
- package/test/theme.test.js +87 -19
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/users-web",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.20",
|
|
5
5
|
description: "Users web module: workspace selector shell element plus workspace/profile/members UI elements.",
|
|
6
6
|
dependsOn: [
|
|
7
7
|
"@jskit-ai/http-runtime",
|
|
@@ -241,11 +241,11 @@ export default Object.freeze({
|
|
|
241
241
|
"@uppy/dashboard": "^5.1.1",
|
|
242
242
|
"@uppy/image-editor": "^4.2.0",
|
|
243
243
|
"@uppy/xhr-upload": "^5.1.1",
|
|
244
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
245
|
-
"@jskit-ai/realtime": "0.1.
|
|
246
|
-
"@jskit-ai/kernel": "0.1.
|
|
247
|
-
"@jskit-ai/shell-web": "0.1.
|
|
248
|
-
"@jskit-ai/users-core": "0.1.
|
|
244
|
+
"@jskit-ai/http-runtime": "0.1.11",
|
|
245
|
+
"@jskit-ai/realtime": "0.1.11",
|
|
246
|
+
"@jskit-ai/kernel": "0.1.11",
|
|
247
|
+
"@jskit-ai/shell-web": "0.1.11",
|
|
248
|
+
"@jskit-ai/users-core": "0.1.16",
|
|
249
249
|
"vuetify": "^4.0.0"
|
|
250
250
|
},
|
|
251
251
|
dev: {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@tanstack/vue-query": "5.92.12",
|
|
23
23
|
"@mdi/js": "^7.4.47",
|
|
24
|
-
"@jskit-ai/users-core": "0.1.
|
|
25
|
-
"@jskit-ai/realtime": "0.1.
|
|
26
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
27
|
-
"@jskit-ai/kernel": "0.1.
|
|
28
|
-
"@jskit-ai/shell-web": "0.1.
|
|
24
|
+
"@jskit-ai/users-core": "0.1.16",
|
|
25
|
+
"@jskit-ai/realtime": "0.1.11",
|
|
26
|
+
"@jskit-ai/http-runtime": "0.1.11",
|
|
27
|
+
"@jskit-ai/kernel": "0.1.11",
|
|
28
|
+
"@jskit-ai/shell-web": "0.1.11",
|
|
29
29
|
"vuetify": "^4.0.0"
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -50,53 +50,127 @@
|
|
|
50
50
|
|
|
51
51
|
<v-col cols="12">
|
|
52
52
|
<div class="text-subtitle-2 mb-2">Theme colors</div>
|
|
53
|
+
<v-row dense class="mb-2">
|
|
54
|
+
<v-col cols="12">
|
|
55
|
+
<div class="text-caption text-medium-emphasis mb-2">Light palette</div>
|
|
56
|
+
</v-col>
|
|
57
|
+
<v-col cols="12" md="3">
|
|
58
|
+
<v-text-field
|
|
59
|
+
v-model="workspaceForm.lightPrimaryColor"
|
|
60
|
+
label="Primary"
|
|
61
|
+
type="color"
|
|
62
|
+
variant="outlined"
|
|
63
|
+
density="comfortable"
|
|
64
|
+
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
65
|
+
:error-messages="
|
|
66
|
+
addEdit.fieldErrors.lightPrimaryColor ? [addEdit.fieldErrors.lightPrimaryColor] : []
|
|
67
|
+
"
|
|
68
|
+
/>
|
|
69
|
+
</v-col>
|
|
70
|
+
|
|
71
|
+
<v-col cols="12" md="3">
|
|
72
|
+
<v-text-field
|
|
73
|
+
v-model="workspaceForm.lightSecondaryColor"
|
|
74
|
+
label="Secondary"
|
|
75
|
+
type="color"
|
|
76
|
+
variant="outlined"
|
|
77
|
+
density="comfortable"
|
|
78
|
+
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
79
|
+
:error-messages="
|
|
80
|
+
addEdit.fieldErrors.lightSecondaryColor ? [addEdit.fieldErrors.lightSecondaryColor] : []
|
|
81
|
+
"
|
|
82
|
+
/>
|
|
83
|
+
</v-col>
|
|
84
|
+
|
|
85
|
+
<v-col cols="12" md="3">
|
|
86
|
+
<v-text-field
|
|
87
|
+
v-model="workspaceForm.lightSurfaceColor"
|
|
88
|
+
label="Surface"
|
|
89
|
+
type="color"
|
|
90
|
+
variant="outlined"
|
|
91
|
+
density="comfortable"
|
|
92
|
+
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
93
|
+
:error-messages="
|
|
94
|
+
addEdit.fieldErrors.lightSurfaceColor ? [addEdit.fieldErrors.lightSurfaceColor] : []
|
|
95
|
+
"
|
|
96
|
+
/>
|
|
97
|
+
</v-col>
|
|
98
|
+
|
|
99
|
+
<v-col cols="12" md="3">
|
|
100
|
+
<v-text-field
|
|
101
|
+
v-model="workspaceForm.lightSurfaceVariantColor"
|
|
102
|
+
label="Surface variant"
|
|
103
|
+
type="color"
|
|
104
|
+
variant="outlined"
|
|
105
|
+
density="comfortable"
|
|
106
|
+
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
107
|
+
:error-messages="
|
|
108
|
+
addEdit.fieldErrors.lightSurfaceVariantColor
|
|
109
|
+
? [addEdit.fieldErrors.lightSurfaceVariantColor]
|
|
110
|
+
: []
|
|
111
|
+
"
|
|
112
|
+
/>
|
|
113
|
+
</v-col>
|
|
114
|
+
</v-row>
|
|
115
|
+
|
|
53
116
|
<v-row dense>
|
|
117
|
+
<v-col cols="12">
|
|
118
|
+
<div class="text-caption text-medium-emphasis mb-2">Dark palette</div>
|
|
119
|
+
</v-col>
|
|
54
120
|
<v-col cols="12" md="3">
|
|
55
121
|
<v-text-field
|
|
56
|
-
v-model="workspaceForm.
|
|
122
|
+
v-model="workspaceForm.darkPrimaryColor"
|
|
57
123
|
label="Primary"
|
|
58
124
|
type="color"
|
|
59
125
|
variant="outlined"
|
|
60
126
|
density="comfortable"
|
|
61
127
|
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
62
|
-
:error-messages="
|
|
128
|
+
:error-messages="
|
|
129
|
+
addEdit.fieldErrors.darkPrimaryColor ? [addEdit.fieldErrors.darkPrimaryColor] : []
|
|
130
|
+
"
|
|
63
131
|
/>
|
|
64
132
|
</v-col>
|
|
65
133
|
|
|
66
134
|
<v-col cols="12" md="3">
|
|
67
135
|
<v-text-field
|
|
68
|
-
v-model="workspaceForm.
|
|
136
|
+
v-model="workspaceForm.darkSecondaryColor"
|
|
69
137
|
label="Secondary"
|
|
70
138
|
type="color"
|
|
71
139
|
variant="outlined"
|
|
72
140
|
density="comfortable"
|
|
73
141
|
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
74
|
-
:error-messages="
|
|
142
|
+
:error-messages="
|
|
143
|
+
addEdit.fieldErrors.darkSecondaryColor ? [addEdit.fieldErrors.darkSecondaryColor] : []
|
|
144
|
+
"
|
|
75
145
|
/>
|
|
76
146
|
</v-col>
|
|
77
147
|
|
|
78
148
|
<v-col cols="12" md="3">
|
|
79
149
|
<v-text-field
|
|
80
|
-
v-model="workspaceForm.
|
|
150
|
+
v-model="workspaceForm.darkSurfaceColor"
|
|
81
151
|
label="Surface"
|
|
82
152
|
type="color"
|
|
83
153
|
variant="outlined"
|
|
84
154
|
density="comfortable"
|
|
85
155
|
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
86
|
-
:error-messages="
|
|
156
|
+
:error-messages="
|
|
157
|
+
addEdit.fieldErrors.darkSurfaceColor ? [addEdit.fieldErrors.darkSurfaceColor] : []
|
|
158
|
+
"
|
|
87
159
|
/>
|
|
88
160
|
</v-col>
|
|
89
161
|
|
|
90
162
|
<v-col cols="12" md="3">
|
|
91
163
|
<v-text-field
|
|
92
|
-
v-model="workspaceForm.
|
|
164
|
+
v-model="workspaceForm.darkSurfaceVariantColor"
|
|
93
165
|
label="Surface variant"
|
|
94
166
|
type="color"
|
|
95
167
|
variant="outlined"
|
|
96
168
|
density="comfortable"
|
|
97
169
|
:readonly="!addEdit.canSave || addEdit.isSaving || addEdit.isRefetching"
|
|
98
170
|
:error-messages="
|
|
99
|
-
addEdit.fieldErrors.
|
|
171
|
+
addEdit.fieldErrors.darkSurfaceVariantColor
|
|
172
|
+
? [addEdit.fieldErrors.darkSurfaceVariantColor]
|
|
173
|
+
: []
|
|
100
174
|
"
|
|
101
175
|
/>
|
|
102
176
|
</v-col>
|
|
@@ -140,8 +214,9 @@ import { workspaceSettingsResource } from "@jskit-ai/users-core/shared/resources
|
|
|
140
214
|
import { WORKSPACE_SETTINGS_CHANGED_EVENT } from "@jskit-ai/users-core/shared/events/usersEvents";
|
|
141
215
|
import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
|
|
142
216
|
import {
|
|
143
|
-
|
|
144
|
-
|
|
217
|
+
DEFAULT_WORKSPACE_DARK_PALETTE,
|
|
218
|
+
DEFAULT_WORKSPACE_LIGHT_PALETTE,
|
|
219
|
+
resolveWorkspaceThemePalettes
|
|
145
220
|
} from "@jskit-ai/users-core/shared/settings";
|
|
146
221
|
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
147
222
|
import { useAddEdit } from "../composables/useAddEdit.js";
|
|
@@ -152,16 +227,16 @@ import { arePermissionListsEqual, normalizePermissionList } from "../lib/permiss
|
|
|
152
227
|
import { createWorkspaceRealtimeMatcher } from "../support/realtimeWorkspace.js";
|
|
153
228
|
import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
|
|
154
229
|
|
|
155
|
-
const DEFAULT_WORKSPACE_THEME = resolveWorkspaceThemePalette({
|
|
156
|
-
color: DEFAULT_WORKSPACE_COLOR
|
|
157
|
-
});
|
|
158
|
-
|
|
159
230
|
const workspaceForm = reactive({
|
|
160
231
|
name: "",
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
232
|
+
lightPrimaryColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.color,
|
|
233
|
+
lightSecondaryColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.secondaryColor,
|
|
234
|
+
lightSurfaceColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceColor,
|
|
235
|
+
lightSurfaceVariantColor: DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceVariantColor,
|
|
236
|
+
darkPrimaryColor: DEFAULT_WORKSPACE_DARK_PALETTE.color,
|
|
237
|
+
darkSecondaryColor: DEFAULT_WORKSPACE_DARK_PALETTE.secondaryColor,
|
|
238
|
+
darkSurfaceColor: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceColor,
|
|
239
|
+
darkSurfaceVariantColor: DEFAULT_WORKSPACE_DARK_PALETTE.surfaceVariantColor,
|
|
165
240
|
avatarUrl: "",
|
|
166
241
|
invitesEnabled: false,
|
|
167
242
|
invitesAvailable: false
|
|
@@ -195,12 +270,16 @@ function toWorkspaceSettingsSnapshot(settings = null) {
|
|
|
195
270
|
return "";
|
|
196
271
|
}
|
|
197
272
|
|
|
198
|
-
const normalized =
|
|
273
|
+
const normalized = resolveWorkspaceThemePalettes(settings);
|
|
199
274
|
return JSON.stringify({
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
275
|
+
lightPrimaryColor: normalized.light.color,
|
|
276
|
+
lightSecondaryColor: normalized.light.secondaryColor,
|
|
277
|
+
lightSurfaceColor: normalized.light.surfaceColor,
|
|
278
|
+
lightSurfaceVariantColor: normalized.light.surfaceVariantColor,
|
|
279
|
+
darkPrimaryColor: normalized.dark.color,
|
|
280
|
+
darkSecondaryColor: normalized.dark.secondaryColor,
|
|
281
|
+
darkSurfaceColor: normalized.dark.surfaceColor,
|
|
282
|
+
darkSurfaceVariantColor: normalized.dark.surfaceVariantColor,
|
|
204
283
|
invitesEnabled: settings.invitesEnabled !== false,
|
|
205
284
|
invitesAvailable: settings.invitesAvailable !== false
|
|
206
285
|
});
|
|
@@ -267,7 +346,18 @@ const addEdit = useAddEdit({
|
|
|
267
346
|
savePermissions: ["workspace.settings.update"],
|
|
268
347
|
placementSource: "users-web.workspace-settings-view",
|
|
269
348
|
fallbackLoadError: "Unable to load workspace settings.",
|
|
270
|
-
fieldErrorKeys: [
|
|
349
|
+
fieldErrorKeys: [
|
|
350
|
+
"name",
|
|
351
|
+
"avatarUrl",
|
|
352
|
+
"lightPrimaryColor",
|
|
353
|
+
"lightSecondaryColor",
|
|
354
|
+
"lightSurfaceColor",
|
|
355
|
+
"lightSurfaceVariantColor",
|
|
356
|
+
"darkPrimaryColor",
|
|
357
|
+
"darkSecondaryColor",
|
|
358
|
+
"darkSurfaceColor",
|
|
359
|
+
"darkSurfaceVariantColor"
|
|
360
|
+
],
|
|
271
361
|
realtime: {
|
|
272
362
|
event: WORKSPACE_SETTINGS_CHANGED_EVENT,
|
|
273
363
|
matches: matchesWorkspaceRealtime
|
|
@@ -281,23 +371,31 @@ const addEdit = useAddEdit({
|
|
|
281
371
|
}),
|
|
282
372
|
mapLoadedToModel: (model, payload = {}) => {
|
|
283
373
|
const settings = payload?.settings && typeof payload.settings === "object" ? payload.settings : {};
|
|
284
|
-
const normalizedTheme =
|
|
374
|
+
const normalizedTheme = resolveWorkspaceThemePalettes(settings);
|
|
285
375
|
|
|
286
376
|
model.name = String(settings.name || "");
|
|
287
|
-
model.
|
|
288
|
-
model.
|
|
289
|
-
model.
|
|
290
|
-
model.
|
|
377
|
+
model.lightPrimaryColor = normalizedTheme.light.color;
|
|
378
|
+
model.lightSecondaryColor = normalizedTheme.light.secondaryColor;
|
|
379
|
+
model.lightSurfaceColor = normalizedTheme.light.surfaceColor;
|
|
380
|
+
model.lightSurfaceVariantColor = normalizedTheme.light.surfaceVariantColor;
|
|
381
|
+
model.darkPrimaryColor = normalizedTheme.dark.color;
|
|
382
|
+
model.darkSecondaryColor = normalizedTheme.dark.secondaryColor;
|
|
383
|
+
model.darkSurfaceColor = normalizedTheme.dark.surfaceColor;
|
|
384
|
+
model.darkSurfaceVariantColor = normalizedTheme.dark.surfaceVariantColor;
|
|
291
385
|
model.avatarUrl = String(settings.avatarUrl || "");
|
|
292
386
|
model.invitesEnabled = settings.invitesEnabled !== false;
|
|
293
387
|
model.invitesAvailable = settings.invitesAvailable !== false;
|
|
294
388
|
},
|
|
295
389
|
buildRawPayload: (model) => ({
|
|
296
390
|
name: model.name,
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
391
|
+
lightPrimaryColor: model.lightPrimaryColor,
|
|
392
|
+
lightSecondaryColor: model.lightSecondaryColor,
|
|
393
|
+
lightSurfaceColor: model.lightSurfaceColor,
|
|
394
|
+
lightSurfaceVariantColor: model.lightSurfaceVariantColor,
|
|
395
|
+
darkPrimaryColor: model.darkPrimaryColor,
|
|
396
|
+
darkSecondaryColor: model.darkSecondaryColor,
|
|
397
|
+
darkSurfaceColor: model.darkSurfaceColor,
|
|
398
|
+
darkSurfaceVariantColor: model.darkSurfaceVariantColor,
|
|
301
399
|
avatarUrl: model.avatarUrl,
|
|
302
400
|
invitesEnabled: model.invitesEnabled
|
|
303
401
|
}),
|
|
@@ -12,6 +12,7 @@ import { userProfileResource } from "@jskit-ai/users-core/shared/resources/userP
|
|
|
12
12
|
import { userSettingsResource } from "@jskit-ai/users-core/shared/resources/userSettingsResource";
|
|
13
13
|
import { USERS_ROUTE_VISIBILITY_PUBLIC } from "@jskit-ai/users-core/shared/support/usersVisibility";
|
|
14
14
|
import {
|
|
15
|
+
persistThemePreference,
|
|
15
16
|
resolveThemeNameForPreference,
|
|
16
17
|
setVuetifyThemeName
|
|
17
18
|
} from "../lib/theme.js";
|
|
@@ -151,6 +152,7 @@ function useAccountSettingsRuntime() {
|
|
|
151
152
|
function applyThemePreference(themePreference) {
|
|
152
153
|
const themeName = resolveThemeNameForPreference(themePreference);
|
|
153
154
|
setVuetifyThemeName(vuetifyTheme, themeName);
|
|
155
|
+
persistThemePreference(themePreference);
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
function applyAvatarData(avatar) {
|
package/src/client/lib/theme.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveWorkspaceThemePalette } from "@jskit-ai/users-core/shared/settin
|
|
|
4
4
|
const THEME_PREFERENCE_LIGHT = "light";
|
|
5
5
|
const THEME_PREFERENCE_DARK = "dark";
|
|
6
6
|
const THEME_PREFERENCE_SYSTEM = "system";
|
|
7
|
+
const THEME_PREFERENCE_STORAGE_KEY = "jskit.themePreference";
|
|
7
8
|
const HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
|
|
8
9
|
const WORKSPACE_THEME_NAME_LIGHT = "workspace-light";
|
|
9
10
|
const WORKSPACE_THEME_NAME_DARK = "workspace-dark";
|
|
@@ -41,15 +42,80 @@ function resolveThemeNameForPreference(themePreference, options = {}) {
|
|
|
41
42
|
return resolveSystemThemeName(options);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
function resolveThemePreferenceStorage(options = {}) {
|
|
46
|
+
const customStorage = options && typeof options === "object" ? options.storage : null;
|
|
47
|
+
if (customStorage && typeof customStorage === "object") {
|
|
48
|
+
return customStorage;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof window !== "object" || !window) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let storage = null;
|
|
56
|
+
try {
|
|
57
|
+
storage = window.localStorage;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (!storage || typeof storage !== "object") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return storage;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readPersistedThemePreference(options = {}) {
|
|
68
|
+
const storage = resolveThemePreferenceStorage(options);
|
|
69
|
+
if (!storage || typeof storage.getItem !== "function") {
|
|
70
|
+
return THEME_PREFERENCE_SYSTEM;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const value = storage.getItem(THEME_PREFERENCE_STORAGE_KEY);
|
|
75
|
+
return normalizeThemePreference(value);
|
|
76
|
+
} catch {
|
|
77
|
+
return THEME_PREFERENCE_SYSTEM;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function persistThemePreference(themePreference, options = {}) {
|
|
82
|
+
const storage = resolveThemePreferenceStorage(options);
|
|
83
|
+
if (!storage || typeof storage.setItem !== "function") {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const normalizedPreference = normalizeThemePreference(themePreference);
|
|
88
|
+
try {
|
|
89
|
+
storage.setItem(THEME_PREFERENCE_STORAGE_KEY, normalizedPreference);
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveBootstrapThemePreference(payload = {}, options = {}) {
|
|
97
|
+
const source = payload && typeof payload === "object" ? payload : {};
|
|
98
|
+
const session = source.session && typeof source.session === "object" ? source.session : {};
|
|
99
|
+
if (session.authenticated === true) {
|
|
100
|
+
const userSettings = source.userSettings && typeof source.userSettings === "object" ? source.userSettings : {};
|
|
101
|
+
return normalizeThemePreference(userSettings.theme);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return readPersistedThemePreference(options);
|
|
105
|
+
}
|
|
106
|
+
|
|
44
107
|
function resolveBootstrapThemeName(payload = {}, options = {}) {
|
|
108
|
+
return resolveThemeNameForPreference(resolveBootstrapThemePreference(payload, options), options);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function persistBootstrapThemePreference(payload = {}, options = {}) {
|
|
45
112
|
const source = payload && typeof payload === "object" ? payload : {};
|
|
46
113
|
const session = source.session && typeof source.session === "object" ? source.session : {};
|
|
47
114
|
if (session.authenticated !== true) {
|
|
48
|
-
return
|
|
115
|
+
return false;
|
|
49
116
|
}
|
|
50
117
|
|
|
51
|
-
|
|
52
|
-
return resolveThemeNameForPreference(userSettings.theme, options);
|
|
118
|
+
return persistThemePreference(resolveBootstrapThemePreference(payload, options), options);
|
|
53
119
|
}
|
|
54
120
|
|
|
55
121
|
function resolveVuetifyThemeController(vueApp) {
|
|
@@ -175,8 +241,7 @@ function composeWorkspaceThemeDefinition(baseThemeDefinition, palette) {
|
|
|
175
241
|
primary: palette.color,
|
|
176
242
|
secondary: palette.secondaryColor,
|
|
177
243
|
surface: palette.surfaceColor,
|
|
178
|
-
"surface-variant": palette.surfaceVariantColor
|
|
179
|
-
background: palette.backgroundColor
|
|
244
|
+
"surface-variant": palette.surfaceVariantColor
|
|
180
245
|
}
|
|
181
246
|
};
|
|
182
247
|
}
|
|
@@ -235,9 +300,10 @@ function setVuetifyPrimaryColorOverride(themeController, themeInput = null) {
|
|
|
235
300
|
return false;
|
|
236
301
|
}
|
|
237
302
|
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
const
|
|
303
|
+
const lightPalette = resolveWorkspaceThemePalette(source, { mode: THEME_PREFERENCE_LIGHT });
|
|
304
|
+
const darkPalette = resolveWorkspaceThemePalette(source, { mode: THEME_PREFERENCE_DARK });
|
|
305
|
+
const nextLightTheme = composeWorkspaceThemeDefinition(baseLightTheme, lightPalette);
|
|
306
|
+
const nextDarkTheme = composeWorkspaceThemeDefinition(baseDarkTheme, darkPalette);
|
|
241
307
|
const nextThemeName =
|
|
242
308
|
normalizedThemeName === THEME_PREFERENCE_DARK ? WORKSPACE_THEME_NAME_DARK : WORKSPACE_THEME_NAME_LIGHT;
|
|
243
309
|
|
|
@@ -255,6 +321,10 @@ function setVuetifyPrimaryColorOverride(themeController, themeInput = null) {
|
|
|
255
321
|
export {
|
|
256
322
|
hexColorToRgb,
|
|
257
323
|
normalizeThemePreference,
|
|
324
|
+
persistBootstrapThemePreference,
|
|
325
|
+
persistThemePreference,
|
|
326
|
+
readPersistedThemePreference,
|
|
327
|
+
resolveBootstrapThemePreference,
|
|
258
328
|
resolveThemeNameForPreference,
|
|
259
329
|
resolveBootstrapThemeName,
|
|
260
330
|
resolveVuetifyThemeController,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "../lib/bootstrap.js";
|
|
13
13
|
import { normalizePermissionList } from "../lib/permissions.js";
|
|
14
14
|
import {
|
|
15
|
+
persistBootstrapThemePreference,
|
|
15
16
|
resolveBootstrapThemeName,
|
|
16
17
|
setVuetifyPrimaryColorOverride,
|
|
17
18
|
resolveVuetifyThemeController,
|
|
@@ -202,6 +203,7 @@ function createBootstrapPlacementRuntime({ app, logger = null, fetchBootstrap =
|
|
|
202
203
|
try {
|
|
203
204
|
const nextThemeName = resolveBootstrapThemeName(payload);
|
|
204
205
|
setVuetifyThemeName(themeController, nextThemeName);
|
|
206
|
+
persistBootstrapThemePreference(payload);
|
|
205
207
|
} catch (error) {
|
|
206
208
|
runtimeLogger.warn(
|
|
207
209
|
{
|
|
@@ -478,10 +478,23 @@ test("bootstrap placement runtime refetches when auth context changes", async ()
|
|
|
478
478
|
assert.deepEqual(fetchCalls, ["acme", "acme"]);
|
|
479
479
|
});
|
|
480
480
|
|
|
481
|
-
test("bootstrap placement runtime
|
|
481
|
+
test("bootstrap placement runtime applies persisted theme preference for unauthenticated bootstrap payloads", async () => {
|
|
482
482
|
const placementRuntime = createPlacementRuntimeStub();
|
|
483
483
|
const router = createRouterStub("/auth/login");
|
|
484
484
|
const themeController = createVuetifyThemeController("dark");
|
|
485
|
+
const storage = new Map();
|
|
486
|
+
storage.set("jskit.themePreference", "dark");
|
|
487
|
+
const originalWindow = globalThis.window;
|
|
488
|
+
globalThis.window = {
|
|
489
|
+
localStorage: {
|
|
490
|
+
getItem(key) {
|
|
491
|
+
return storage.get(key) || null;
|
|
492
|
+
},
|
|
493
|
+
setItem(key, value) {
|
|
494
|
+
storage.set(key, value);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
};
|
|
485
498
|
const runtime = createBootstrapPlacementRuntime({
|
|
486
499
|
app: createAppStub({
|
|
487
500
|
[WEB_PLACEMENT_RUNTIME_CLIENT_TOKEN]: placementRuntime,
|
|
@@ -499,8 +512,16 @@ test("bootstrap placement runtime forces light theme for unauthenticated bootstr
|
|
|
499
512
|
}
|
|
500
513
|
});
|
|
501
514
|
|
|
502
|
-
|
|
503
|
-
|
|
515
|
+
try {
|
|
516
|
+
await runtime.initialize();
|
|
517
|
+
assert.equal(themeController.global.name.value, "dark");
|
|
518
|
+
} finally {
|
|
519
|
+
if (typeof originalWindow === "undefined") {
|
|
520
|
+
delete globalThis.window;
|
|
521
|
+
} else {
|
|
522
|
+
globalThis.window = originalWindow;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
504
525
|
});
|
|
505
526
|
|
|
506
527
|
test("bootstrap placement runtime reapplies theme when bootstrap payload changes", async () => {
|
|
@@ -563,12 +584,21 @@ test("bootstrap placement runtime applies workspace palette via Vuetify workspac
|
|
|
563
584
|
authenticated: true,
|
|
564
585
|
userId: 1
|
|
565
586
|
},
|
|
587
|
+
workspaceSettings: {
|
|
588
|
+
lightPrimaryColor: "#CC3344",
|
|
589
|
+
lightSecondaryColor: "#884455",
|
|
590
|
+
lightSurfaceColor: "#F4F4F4",
|
|
591
|
+
lightSurfaceVariantColor: "#444444",
|
|
592
|
+
darkPrimaryColor: "#BB2233",
|
|
593
|
+
darkSecondaryColor: "#557799",
|
|
594
|
+
darkSurfaceColor: "#202020",
|
|
595
|
+
darkSurfaceVariantColor: "#A0A0A0"
|
|
596
|
+
},
|
|
566
597
|
workspaces: [
|
|
567
598
|
{
|
|
568
599
|
id: 1,
|
|
569
600
|
slug: "acme",
|
|
570
|
-
name: "Acme Workspace"
|
|
571
|
-
color: "#CC3344"
|
|
601
|
+
name: "Acme Workspace"
|
|
572
602
|
}
|
|
573
603
|
],
|
|
574
604
|
permissions: []
|
|
@@ -578,7 +608,12 @@ test("bootstrap placement runtime applies workspace palette via Vuetify workspac
|
|
|
578
608
|
|
|
579
609
|
await runtime.initialize();
|
|
580
610
|
const palette = resolveWorkspaceThemePalette({
|
|
581
|
-
|
|
611
|
+
lightPrimaryColor: "#CC3344",
|
|
612
|
+
lightSecondaryColor: "#884455",
|
|
613
|
+
lightSurfaceColor: "#F4F4F4",
|
|
614
|
+
lightSurfaceVariantColor: "#444444"
|
|
615
|
+
}, {
|
|
616
|
+
mode: "light"
|
|
582
617
|
});
|
|
583
618
|
assert.equal(themeController.global.name.value, "workspace-light");
|
|
584
619
|
assert.equal(themeController.themes.value["workspace-light"].colors.primary, palette.color);
|
|
@@ -588,7 +623,6 @@ test("bootstrap placement runtime applies workspace palette via Vuetify workspac
|
|
|
588
623
|
themeController.themes.value["workspace-light"].colors["surface-variant"],
|
|
589
624
|
palette.surfaceVariantColor
|
|
590
625
|
);
|
|
591
|
-
assert.equal(themeController.themes.value["workspace-light"].colors.background, palette.backgroundColor);
|
|
592
626
|
|
|
593
627
|
router.currentRoute.value.path = "/home";
|
|
594
628
|
router.currentRoute.value.fullPath = "/home";
|
package/test/theme.test.js
CHANGED
|
@@ -5,6 +5,10 @@ import { resolveWorkspaceThemePalette } from "@jskit-ai/users-core/shared/settin
|
|
|
5
5
|
import {
|
|
6
6
|
hexColorToRgb,
|
|
7
7
|
normalizeThemePreference,
|
|
8
|
+
persistBootstrapThemePreference,
|
|
9
|
+
persistThemePreference,
|
|
10
|
+
readPersistedThemePreference,
|
|
11
|
+
resolveBootstrapThemePreference,
|
|
8
12
|
resolveThemeNameForPreference,
|
|
9
13
|
resolveBootstrapThemeName,
|
|
10
14
|
resolveVuetifyThemeController,
|
|
@@ -60,16 +64,23 @@ test("resolveThemeNameForPreference resolves system using explicit prefersDark",
|
|
|
60
64
|
assert.equal(resolveThemeNameForPreference("light", { prefersDark: true }), "light");
|
|
61
65
|
});
|
|
62
66
|
|
|
63
|
-
test("resolveBootstrapThemeName
|
|
67
|
+
test("resolveBootstrapThemeName uses persisted preference for unauthenticated payloads", () => {
|
|
68
|
+
const storage = new Map();
|
|
69
|
+
storage.set("jskit.themePreference", "dark");
|
|
70
|
+
|
|
64
71
|
assert.equal(
|
|
65
|
-
resolveBootstrapThemeName(
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
resolveBootstrapThemeName({
|
|
73
|
+
session: { authenticated: false },
|
|
74
|
+
userSettings: { theme: "light" }
|
|
75
|
+
}, {
|
|
76
|
+
storage: {
|
|
77
|
+
getItem(key) {
|
|
78
|
+
return storage.get(key) || null;
|
|
79
|
+
}
|
|
69
80
|
},
|
|
70
|
-
|
|
71
|
-
),
|
|
72
|
-
"
|
|
81
|
+
prefersDark: false
|
|
82
|
+
}),
|
|
83
|
+
"dark"
|
|
73
84
|
);
|
|
74
85
|
});
|
|
75
86
|
|
|
@@ -96,6 +107,45 @@ test("resolveBootstrapThemeName uses authenticated user preference", () => {
|
|
|
96
107
|
);
|
|
97
108
|
});
|
|
98
109
|
|
|
110
|
+
test("theme preference persistence helpers normalize values", () => {
|
|
111
|
+
const storage = new Map();
|
|
112
|
+
const storageAdapter = {
|
|
113
|
+
getItem(key) {
|
|
114
|
+
return storage.get(key) || null;
|
|
115
|
+
},
|
|
116
|
+
setItem(key, value) {
|
|
117
|
+
storage.set(key, value);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
assert.equal(readPersistedThemePreference({ storage: storageAdapter }), "system");
|
|
122
|
+
assert.equal(persistThemePreference(" DARK ", { storage: storageAdapter }), true);
|
|
123
|
+
assert.equal(readPersistedThemePreference({ storage: storageAdapter }), "dark");
|
|
124
|
+
|
|
125
|
+
assert.equal(
|
|
126
|
+
resolveBootstrapThemePreference(
|
|
127
|
+
{
|
|
128
|
+
session: { authenticated: false }
|
|
129
|
+
},
|
|
130
|
+
{ storage: storageAdapter }
|
|
131
|
+
),
|
|
132
|
+
"dark"
|
|
133
|
+
);
|
|
134
|
+
assert.equal(
|
|
135
|
+
persistBootstrapThemePreference(
|
|
136
|
+
{
|
|
137
|
+
session: { authenticated: true },
|
|
138
|
+
userSettings: {
|
|
139
|
+
theme: "light"
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{ storage: storageAdapter }
|
|
143
|
+
),
|
|
144
|
+
true
|
|
145
|
+
);
|
|
146
|
+
assert.equal(readPersistedThemePreference({ storage: storageAdapter }), "light");
|
|
147
|
+
});
|
|
148
|
+
|
|
99
149
|
test("resolveVuetifyThemeController reads theme controller from Vue app provides", () => {
|
|
100
150
|
const themeController = createVuetifyThemeController("light");
|
|
101
151
|
const vueApp = {
|
|
@@ -126,27 +176,45 @@ test("hexColorToRgb returns Vuetify rgb tuple and rejects invalid values", () =>
|
|
|
126
176
|
|
|
127
177
|
test("setVuetifyPrimaryColorOverride mutates workspace themes and restores base theme names", () => {
|
|
128
178
|
const themeController = createVuetifyThemeController("light");
|
|
129
|
-
const
|
|
130
|
-
|
|
179
|
+
const themeInput = {
|
|
180
|
+
lightPrimaryColor: "#CC3344",
|
|
181
|
+
lightSecondaryColor: "#884455",
|
|
182
|
+
lightSurfaceColor: "#F4F4F4",
|
|
183
|
+
lightSurfaceVariantColor: "#444444",
|
|
184
|
+
darkPrimaryColor: "#BB2233",
|
|
185
|
+
darkSecondaryColor: "#557799",
|
|
186
|
+
darkSurfaceColor: "#202020",
|
|
187
|
+
darkSurfaceVariantColor: "#A0A0A0"
|
|
188
|
+
};
|
|
189
|
+
const expectedLightPalette = resolveWorkspaceThemePalette(themeInput, {
|
|
190
|
+
mode: "light"
|
|
191
|
+
});
|
|
192
|
+
const expectedDarkPalette = resolveWorkspaceThemePalette(themeInput, {
|
|
193
|
+
mode: "dark"
|
|
131
194
|
});
|
|
132
195
|
|
|
133
|
-
assert.equal(setVuetifyPrimaryColorOverride(themeController,
|
|
196
|
+
assert.equal(setVuetifyPrimaryColorOverride(themeController, themeInput), true);
|
|
134
197
|
assert.equal(themeController.global.name.value, "workspace-light");
|
|
135
|
-
assert.equal(themeController.themes.value["workspace-light"].colors.primary,
|
|
136
|
-
assert.equal(themeController.themes.value["workspace-light"].colors.secondary,
|
|
137
|
-
assert.equal(themeController.themes.value["workspace-light"].colors.surface,
|
|
198
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.primary, expectedLightPalette.color);
|
|
199
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.secondary, expectedLightPalette.secondaryColor);
|
|
200
|
+
assert.equal(themeController.themes.value["workspace-light"].colors.surface, expectedLightPalette.surfaceColor);
|
|
138
201
|
assert.equal(
|
|
139
202
|
themeController.themes.value["workspace-light"].colors["surface-variant"],
|
|
140
|
-
|
|
203
|
+
expectedLightPalette.surfaceVariantColor
|
|
141
204
|
);
|
|
142
|
-
assert.equal(themeController.themes.value["workspace-light"].colors.background, expectedPalette.backgroundColor);
|
|
143
205
|
|
|
144
|
-
assert.equal(setVuetifyPrimaryColorOverride(themeController,
|
|
206
|
+
assert.equal(setVuetifyPrimaryColorOverride(themeController, themeInput), false);
|
|
145
207
|
|
|
146
208
|
assert.equal(setVuetifyThemeName(themeController, "dark"), true);
|
|
147
|
-
assert.equal(setVuetifyPrimaryColorOverride(themeController,
|
|
209
|
+
assert.equal(setVuetifyPrimaryColorOverride(themeController, themeInput), true);
|
|
148
210
|
assert.equal(themeController.global.name.value, "workspace-dark");
|
|
149
|
-
assert.equal(themeController.themes.value["workspace-dark"].colors.primary,
|
|
211
|
+
assert.equal(themeController.themes.value["workspace-dark"].colors.primary, expectedDarkPalette.color);
|
|
212
|
+
assert.equal(themeController.themes.value["workspace-dark"].colors.secondary, expectedDarkPalette.secondaryColor);
|
|
213
|
+
assert.equal(themeController.themes.value["workspace-dark"].colors.surface, expectedDarkPalette.surfaceColor);
|
|
214
|
+
assert.equal(
|
|
215
|
+
themeController.themes.value["workspace-dark"].colors["surface-variant"],
|
|
216
|
+
expectedDarkPalette.surfaceVariantColor
|
|
217
|
+
);
|
|
150
218
|
|
|
151
219
|
assert.equal(setVuetifyPrimaryColorOverride(themeController, null), true);
|
|
152
220
|
assert.equal(themeController.global.name.value, "dark");
|