@jskit-ai/users-core 0.1.13 → 0.1.16

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/users-core",
4
- version: "0.1.13",
4
+ version: "0.1.16",
5
5
  description: "Users/workspace domain runtime plus HTTP routes for workspace, account, and console features.",
6
6
  dependsOn: [
7
7
  "@jskit-ai/auth-core",
@@ -203,10 +203,10 @@ export default Object.freeze({
203
203
  mutations: {
204
204
  dependencies: {
205
205
  runtime: {
206
- "@jskit-ai/auth-core": "0.1.8",
207
- "@jskit-ai/database-runtime": "0.1.8",
208
- "@jskit-ai/http-runtime": "0.1.8",
209
- "@jskit-ai/kernel": "0.1.8",
206
+ "@jskit-ai/auth-core": "0.1.11",
207
+ "@jskit-ai/database-runtime": "0.1.11",
208
+ "@jskit-ai/http-runtime": "0.1.11",
209
+ "@jskit-ai/kernel": "0.1.11",
210
210
  "@fastify/multipart": "^9.4.0",
211
211
  "@fastify/type-provider-typebox": "^6.1.0",
212
212
  "typebox": "^1.0.81"
@@ -436,7 +436,7 @@ export default Object.freeze({
436
436
  file: "config/server.js",
437
437
  position: "bottom",
438
438
  skipIfContains: "config.workspaceColor =",
439
- value: "\nconfig.workspaceColor = \"indigo\";\n",
439
+ value: "\nconfig.workspaceColor = \"#1867C0\";\n",
440
440
  reason: "Append default server-only users/workspace settings into app-owned config.",
441
441
  category: "users-core",
442
442
  id: "users-core-server-config"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-core",
3
- "version": "0.1.13",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -24,10 +24,10 @@
24
24
  "./shared/resources/consoleSettingsFields": "./src/shared/resources/consoleSettingsFields.js"
25
25
  },
26
26
  "dependencies": {
27
- "@jskit-ai/auth-core": "0.1.8",
28
- "@jskit-ai/database-runtime": "0.1.8",
29
- "@jskit-ai/http-runtime": "0.1.8",
30
- "@jskit-ai/kernel": "0.1.8",
27
+ "@jskit-ai/auth-core": "0.1.11",
28
+ "@jskit-ai/database-runtime": "0.1.11",
29
+ "@jskit-ai/http-runtime": "0.1.11",
30
+ "@jskit-ai/kernel": "0.1.11",
31
31
  "@fastify/multipart": "^9.4.0",
32
32
  "@fastify/type-provider-typebox": "^6.1.0",
33
33
  "typebox": "^1.0.81"
@@ -1,21 +1,15 @@
1
1
  import {
2
2
  coerceWorkspaceColor,
3
- coerceWorkspaceSecondaryColor,
4
- coerceWorkspaceSurfaceColor,
5
- coerceWorkspaceSurfaceVariantColor
3
+ resolveWorkspaceThemePalettes
6
4
  } from "../../../shared/settings.js";
7
5
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
8
6
 
9
7
  function mapWorkspaceSummary(workspace, membership) {
10
- const color = coerceWorkspaceColor(workspace.color);
11
8
  return {
12
9
  id: Number(workspace.id),
13
10
  slug: normalizeText(workspace.slug),
14
11
  name: normalizeText(workspace.name),
15
- color,
16
- secondaryColor: coerceWorkspaceSecondaryColor(workspace.secondaryColor, { color }),
17
- surfaceColor: coerceWorkspaceSurfaceColor(workspace.surfaceColor, { color }),
18
- surfaceVariantColor: coerceWorkspaceSurfaceVariantColor(workspace.surfaceVariantColor, { color }),
12
+ color: coerceWorkspaceColor(workspace.color),
19
13
  avatarUrl: normalizeText(workspace.avatarUrl),
20
14
  roleId: normalizeLowerText(membership?.roleId || "member") || "member",
21
15
  isAccessible: normalizeLowerText(membership?.status || "active") === "active"
@@ -26,13 +20,18 @@ function mapWorkspaceSettingsPublic(workspaceSettings, { workspaceInvitationsEna
26
20
  const source = workspaceSettings && typeof workspaceSettings === "object" ? workspaceSettings : {};
27
21
  const invitesAvailable = workspaceInvitationsEnabled === true;
28
22
  const invitesEnabled = invitesAvailable && source.invitesEnabled !== false;
29
- const color = coerceWorkspaceColor(source.color);
23
+ const themePalettes = resolveWorkspaceThemePalettes(source);
24
+
30
25
  return {
31
26
  name: normalizeText(source.name),
32
- color,
33
- secondaryColor: coerceWorkspaceSecondaryColor(source.secondaryColor, { color }),
34
- surfaceColor: coerceWorkspaceSurfaceColor(source.surfaceColor, { color }),
35
- surfaceVariantColor: coerceWorkspaceSurfaceVariantColor(source.surfaceVariantColor, { color }),
27
+ lightPrimaryColor: themePalettes.light.color,
28
+ lightSecondaryColor: themePalettes.light.secondaryColor,
29
+ lightSurfaceColor: themePalettes.light.surfaceColor,
30
+ lightSurfaceVariantColor: themePalettes.light.surfaceVariantColor,
31
+ darkPrimaryColor: themePalettes.dark.color,
32
+ darkSecondaryColor: themePalettes.dark.secondaryColor,
33
+ darkSurfaceColor: themePalettes.dark.surfaceColor,
34
+ darkSurfaceVariantColor: themePalettes.dark.surfaceVariantColor,
36
35
  avatarUrl: normalizeText(source.avatarUrl),
37
36
  invitesEnabled,
38
37
  invitesAvailable,
@@ -6,12 +6,7 @@ import {
6
6
  nowDb,
7
7
  isDuplicateEntryError
8
8
  } from "./repositoryUtils.js";
9
- import {
10
- coerceWorkspaceColor,
11
- coerceWorkspaceSecondaryColor,
12
- coerceWorkspaceSurfaceColor,
13
- coerceWorkspaceSurfaceVariantColor
14
- } from "../../../shared/settings.js";
9
+ import { coerceWorkspaceColor } from "../../../shared/settings.js";
15
10
 
16
11
  function mapRow(row) {
17
12
  if (!row) {
@@ -27,15 +22,6 @@ function mapRow(row) {
27
22
  isPersonal: Boolean(row.is_personal),
28
23
  avatarUrl: row.avatar_url ? normalizeText(row.avatar_url) : "",
29
24
  color,
30
- secondaryColor: coerceWorkspaceSecondaryColor(row.secondary_color, {
31
- color
32
- }),
33
- surfaceColor: coerceWorkspaceSurfaceColor(row.surface_color, {
34
- color
35
- }),
36
- surfaceVariantColor: coerceWorkspaceSurfaceVariantColor(row.surface_variant_color, {
37
- color
38
- }),
39
25
  createdAt: toIsoString(row.created_at),
40
26
  updatedAt: toIsoString(row.updated_at),
41
27
  deletedAt: toNullableIso(row.deleted_at)
@@ -67,10 +53,7 @@ function createRepository(knex) {
67
53
  "w.owner_user_id",
68
54
  "w.is_personal",
69
55
  client.raw("COALESCE(ws.avatar_url, w.avatar_url) as avatar_url"),
70
- client.raw("COALESCE(ws.color, w.color) as color"),
71
- client.raw("COALESCE(ws.secondary_color, ws.color, w.color) as secondary_color"),
72
- client.raw("COALESCE(ws.surface_color, ws.color, w.color) as surface_color"),
73
- client.raw("COALESCE(ws.surface_variant_color, ws.color, w.color) as surface_variant_color"),
56
+ "w.color",
74
57
  "w.created_at",
75
58
  "w.updated_at",
76
59
  "w.deleted_at"
@@ -8,19 +8,21 @@ import {
8
8
  } from "./roles.js";
9
9
 
10
10
  import {
11
- coerceWorkspaceBackgroundColor,
11
+ DEFAULT_WORKSPACE_DARK_PALETTE,
12
+ DEFAULT_WORKSPACE_LIGHT_PALETTE,
12
13
  DEFAULT_WORKSPACE_COLOR,
13
14
  DEFAULT_USER_SETTINGS,
14
15
  coerceWorkspaceColor,
16
+ coerceWorkspaceThemeColor,
15
17
  coerceWorkspaceSecondaryColor,
16
18
  coerceWorkspaceSurfaceColor,
17
19
  coerceWorkspaceSurfaceVariantColor,
18
- deriveWorkspaceBackgroundColor,
19
- deriveWorkspaceSecondaryColor,
20
- deriveWorkspaceSurfaceColor,
21
- deriveWorkspaceSurfaceVariantColor,
22
- mixHexColors,
23
20
  normalizeWorkspaceHexColor,
21
+ normalizeWorkspaceThemeMode,
22
+ resolveWorkspaceThemeDefaultPalette,
23
+ resolveWorkspaceThemePalettes,
24
+ WORKSPACE_THEME_MODE_DARK,
25
+ WORKSPACE_THEME_MODE_LIGHT,
24
26
  resolveWorkspaceThemePalette
25
27
  } from "./settings.js";
26
28
  import {
@@ -51,19 +53,21 @@ const USERS_SHARED_API = Object.freeze({
51
53
  resolveRolePermissions,
52
54
  listRoleDescriptors,
53
55
  hasPermission,
56
+ DEFAULT_WORKSPACE_LIGHT_PALETTE,
57
+ DEFAULT_WORKSPACE_DARK_PALETTE,
54
58
  DEFAULT_WORKSPACE_COLOR,
55
59
  DEFAULT_USER_SETTINGS,
56
60
  coerceWorkspaceColor,
61
+ coerceWorkspaceThemeColor,
57
62
  coerceWorkspaceSecondaryColor,
58
63
  coerceWorkspaceSurfaceColor,
59
64
  coerceWorkspaceSurfaceVariantColor,
60
- coerceWorkspaceBackgroundColor,
61
- deriveWorkspaceSecondaryColor,
62
- deriveWorkspaceSurfaceColor,
63
- deriveWorkspaceSurfaceVariantColor,
64
- deriveWorkspaceBackgroundColor,
65
+ WORKSPACE_THEME_MODE_LIGHT,
66
+ WORKSPACE_THEME_MODE_DARK,
67
+ normalizeWorkspaceThemeMode,
68
+ resolveWorkspaceThemeDefaultPalette,
69
+ resolveWorkspaceThemePalettes,
65
70
  normalizeWorkspaceHexColor,
66
- mixHexColors,
67
71
  resolveWorkspaceThemePalette,
68
72
  TENANCY_MODE_NONE,
69
73
  TENANCY_MODE_PERSONAL,
@@ -90,19 +94,21 @@ export {
90
94
  resolveRolePermissions,
91
95
  listRoleDescriptors,
92
96
  hasPermission,
97
+ DEFAULT_WORKSPACE_LIGHT_PALETTE,
98
+ DEFAULT_WORKSPACE_DARK_PALETTE,
93
99
  DEFAULT_WORKSPACE_COLOR,
94
100
  DEFAULT_USER_SETTINGS,
95
101
  coerceWorkspaceColor,
102
+ coerceWorkspaceThemeColor,
96
103
  coerceWorkspaceSecondaryColor,
97
104
  coerceWorkspaceSurfaceColor,
98
105
  coerceWorkspaceSurfaceVariantColor,
99
- coerceWorkspaceBackgroundColor,
100
- deriveWorkspaceSecondaryColor,
101
- deriveWorkspaceSurfaceColor,
102
- deriveWorkspaceSurfaceVariantColor,
103
- deriveWorkspaceBackgroundColor,
106
+ WORKSPACE_THEME_MODE_LIGHT,
107
+ WORKSPACE_THEME_MODE_DARK,
108
+ normalizeWorkspaceThemeMode,
109
+ resolveWorkspaceThemeDefaultPalette,
110
+ resolveWorkspaceThemePalettes,
104
111
  normalizeWorkspaceHexColor,
105
- mixHexColors,
106
112
  resolveWorkspaceThemePalette,
107
113
  TENANCY_MODE_NONE,
108
114
  TENANCY_MODE_PERSONAL,
@@ -1,6 +1,23 @@
1
- const DEFAULT_WORKSPACE_COLOR = "#2F5D9E";
1
+ const WORKSPACE_THEME_MODE_LIGHT = "light";
2
+ const WORKSPACE_THEME_MODE_DARK = "dark";
2
3
  const HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
3
4
 
5
+ const DEFAULT_WORKSPACE_LIGHT_PALETTE = Object.freeze({
6
+ color: "#1867C0",
7
+ secondaryColor: "#48A9A6",
8
+ surfaceColor: "#FFFFFF",
9
+ surfaceVariantColor: "#424242"
10
+ });
11
+
12
+ const DEFAULT_WORKSPACE_DARK_PALETTE = Object.freeze({
13
+ color: "#2196F3",
14
+ secondaryColor: "#54B6B2",
15
+ surfaceColor: "#212121",
16
+ surfaceVariantColor: "#C8C8C8"
17
+ });
18
+
19
+ const DEFAULT_WORKSPACE_COLOR = DEFAULT_WORKSPACE_LIGHT_PALETTE.color;
20
+
4
21
  const DEFAULT_USER_SETTINGS = Object.freeze({
5
22
  theme: "system",
6
23
  locale: "en",
@@ -25,124 +42,95 @@ function normalizeWorkspaceHexColor(value) {
25
42
  return normalized.toUpperCase();
26
43
  }
27
44
 
28
- function clampChannel(value) {
29
- if (!Number.isFinite(value)) {
30
- return 0;
31
- }
32
- if (value < 0) {
33
- return 0;
34
- }
35
- if (value > 255) {
36
- return 255;
37
- }
38
- return Math.round(value);
39
- }
40
-
41
- function toHexChannel(value) {
42
- return clampChannel(value).toString(16).padStart(2, "0").toUpperCase();
43
- }
44
-
45
- function parseHexColor(value) {
46
- const normalized = normalizeWorkspaceHexColor(value);
47
- if (!normalized) {
48
- return null;
49
- }
50
- return {
51
- red: Number.parseInt(normalized.slice(1, 3), 16),
52
- green: Number.parseInt(normalized.slice(3, 5), 16),
53
- blue: Number.parseInt(normalized.slice(5, 7), 16)
54
- };
55
- }
56
-
57
- function mixHexColors(baseColor, mixColor, mixRatio = 0) {
58
- const base = parseHexColor(baseColor);
59
- const mixed = parseHexColor(mixColor);
60
- if (!base || !mixed) {
61
- return "";
62
- }
63
-
64
- const ratio = Math.min(1, Math.max(0, Number(mixRatio) || 0));
65
- const keepRatio = 1 - ratio;
66
- return `#${toHexChannel(base.red * keepRatio + mixed.red * ratio)}${toHexChannel(
67
- base.green * keepRatio + mixed.green * ratio
68
- )}${toHexChannel(base.blue * keepRatio + mixed.blue * ratio)}`;
69
- }
70
-
71
45
  function coerceWorkspaceColor(value) {
72
46
  return normalizeWorkspaceHexColor(value) || DEFAULT_WORKSPACE_COLOR;
73
47
  }
74
48
 
75
- function deriveWorkspaceSecondaryColor(workspaceColor = DEFAULT_WORKSPACE_COLOR) {
76
- return mixHexColors(coerceWorkspaceColor(workspaceColor), "#000000", 0.28) || "#224372";
77
- }
78
-
79
- function deriveWorkspaceSurfaceColor(workspaceColor = DEFAULT_WORKSPACE_COLOR) {
80
- return mixHexColors(coerceWorkspaceColor(workspaceColor), "#FFFFFF", 0.93) || "#F0F4F8";
81
- }
82
-
83
- function deriveWorkspaceSurfaceVariantColor(workspaceColor = DEFAULT_WORKSPACE_COLOR) {
84
- return mixHexColors(coerceWorkspaceColor(workspaceColor), "#FFFFFF", 0.86) || "#E2E8F1";
49
+ function normalizeWorkspaceThemeMode(value = "") {
50
+ const normalized = String(value || "").trim().toLowerCase();
51
+ if (normalized === WORKSPACE_THEME_MODE_DARK) {
52
+ return WORKSPACE_THEME_MODE_DARK;
53
+ }
54
+ return WORKSPACE_THEME_MODE_LIGHT;
85
55
  }
86
56
 
87
- function deriveWorkspaceBackgroundColor(workspaceColor = DEFAULT_WORKSPACE_COLOR) {
88
- const surfaceColor = deriveWorkspaceSurfaceColor(workspaceColor);
89
- return mixHexColors(surfaceColor, "#FFFFFF", 0.45) || "#F4FAF8";
57
+ function resolveWorkspaceThemeDefaultPalette(mode = WORKSPACE_THEME_MODE_LIGHT) {
58
+ const normalizedMode = normalizeWorkspaceThemeMode(mode);
59
+ return normalizedMode === WORKSPACE_THEME_MODE_DARK
60
+ ? DEFAULT_WORKSPACE_DARK_PALETTE
61
+ : DEFAULT_WORKSPACE_LIGHT_PALETTE;
90
62
  }
91
63
 
92
- function coerceWorkspaceSecondaryColor(value, { color = DEFAULT_WORKSPACE_COLOR } = {}) {
93
- return normalizeWorkspaceHexColor(value) || deriveWorkspaceSecondaryColor(color);
64
+ function coerceWorkspaceThemeColor(value, fallbackColor = DEFAULT_WORKSPACE_COLOR) {
65
+ return normalizeWorkspaceHexColor(value) || normalizeWorkspaceHexColor(fallbackColor) || DEFAULT_WORKSPACE_COLOR;
94
66
  }
95
67
 
96
- function coerceWorkspaceSurfaceColor(value, { color = DEFAULT_WORKSPACE_COLOR } = {}) {
97
- return normalizeWorkspaceHexColor(value) || deriveWorkspaceSurfaceColor(color);
68
+ function coerceWorkspaceSecondaryColor(value, { mode = WORKSPACE_THEME_MODE_LIGHT } = {}) {
69
+ return coerceWorkspaceThemeColor(value, resolveWorkspaceThemeDefaultPalette(mode).secondaryColor);
98
70
  }
99
71
 
100
- function coerceWorkspaceSurfaceVariantColor(value, { color = DEFAULT_WORKSPACE_COLOR } = {}) {
101
- return normalizeWorkspaceHexColor(value) || deriveWorkspaceSurfaceVariantColor(color);
72
+ function coerceWorkspaceSurfaceColor(value, { mode = WORKSPACE_THEME_MODE_LIGHT } = {}) {
73
+ return coerceWorkspaceThemeColor(value, resolveWorkspaceThemeDefaultPalette(mode).surfaceColor);
102
74
  }
103
75
 
104
- function coerceWorkspaceBackgroundColor(value, { color = DEFAULT_WORKSPACE_COLOR } = {}) {
105
- return normalizeWorkspaceHexColor(value) || deriveWorkspaceBackgroundColor(color);
76
+ function coerceWorkspaceSurfaceVariantColor(value, { mode = WORKSPACE_THEME_MODE_LIGHT } = {}) {
77
+ return coerceWorkspaceThemeColor(value, resolveWorkspaceThemeDefaultPalette(mode).surfaceVariantColor);
106
78
  }
107
79
 
108
- function resolveWorkspaceThemePalette(input = {}) {
80
+ function resolveWorkspaceThemePalette(input = {}, { mode = WORKSPACE_THEME_MODE_LIGHT } = {}) {
109
81
  const source = input && typeof input === "object" ? input : {};
110
- const color = coerceWorkspaceColor(source.color);
111
- const secondaryColor = coerceWorkspaceSecondaryColor(source.secondaryColor, {
112
- color
113
- });
114
- const surfaceColor = coerceWorkspaceSurfaceColor(source.surfaceColor, {
115
- color
116
- });
117
- const surfaceVariantColor = coerceWorkspaceSurfaceVariantColor(source.surfaceVariantColor, {
118
- color
119
- });
120
- const backgroundColor = coerceWorkspaceBackgroundColor(source.backgroundColor, {
121
- color
82
+ const normalizedMode = normalizeWorkspaceThemeMode(mode);
83
+ const paletteDefaults = resolveWorkspaceThemeDefaultPalette(normalizedMode);
84
+
85
+ if (normalizedMode === WORKSPACE_THEME_MODE_DARK) {
86
+ return Object.freeze({
87
+ color: coerceWorkspaceThemeColor(source.darkPrimaryColor, paletteDefaults.color),
88
+ secondaryColor: coerceWorkspaceThemeColor(source.darkSecondaryColor, paletteDefaults.secondaryColor),
89
+ surfaceColor: coerceWorkspaceThemeColor(source.darkSurfaceColor, paletteDefaults.surfaceColor),
90
+ surfaceVariantColor: coerceWorkspaceThemeColor(
91
+ source.darkSurfaceVariantColor,
92
+ paletteDefaults.surfaceVariantColor
93
+ )
94
+ });
95
+ }
96
+
97
+ return Object.freeze({
98
+ color: coerceWorkspaceThemeColor(source.lightPrimaryColor, paletteDefaults.color),
99
+ secondaryColor: coerceWorkspaceThemeColor(source.lightSecondaryColor, paletteDefaults.secondaryColor),
100
+ surfaceColor: coerceWorkspaceThemeColor(source.lightSurfaceColor, paletteDefaults.surfaceColor),
101
+ surfaceVariantColor: coerceWorkspaceThemeColor(
102
+ source.lightSurfaceVariantColor,
103
+ paletteDefaults.surfaceVariantColor
104
+ )
122
105
  });
106
+ }
123
107
 
108
+ function resolveWorkspaceThemePalettes(input = {}) {
124
109
  return Object.freeze({
125
- color,
126
- secondaryColor,
127
- surfaceColor,
128
- surfaceVariantColor,
129
- backgroundColor
110
+ light: resolveWorkspaceThemePalette(input, {
111
+ mode: WORKSPACE_THEME_MODE_LIGHT
112
+ }),
113
+ dark: resolveWorkspaceThemePalette(input, {
114
+ mode: WORKSPACE_THEME_MODE_DARK
115
+ })
130
116
  });
131
117
  }
132
118
 
133
119
  export {
120
+ DEFAULT_WORKSPACE_DARK_PALETTE,
121
+ DEFAULT_WORKSPACE_LIGHT_PALETTE,
134
122
  DEFAULT_WORKSPACE_COLOR,
135
123
  DEFAULT_USER_SETTINGS,
136
- coerceWorkspaceBackgroundColor,
137
124
  coerceWorkspaceColor,
125
+ coerceWorkspaceThemeColor,
138
126
  coerceWorkspaceSecondaryColor,
139
127
  coerceWorkspaceSurfaceColor,
140
128
  coerceWorkspaceSurfaceVariantColor,
141
- deriveWorkspaceBackgroundColor,
142
- deriveWorkspaceSecondaryColor,
143
- deriveWorkspaceSurfaceColor,
144
- deriveWorkspaceSurfaceVariantColor,
145
- mixHexColors,
146
129
  normalizeWorkspaceHexColor,
130
+ normalizeWorkspaceThemeMode,
131
+ resolveWorkspaceThemeDefaultPalette,
132
+ resolveWorkspaceThemePalettes,
133
+ WORKSPACE_THEME_MODE_DARK,
134
+ WORKSPACE_THEME_MODE_LIGHT,
147
135
  resolveWorkspaceThemePalette
148
136
  };
@@ -25,7 +25,7 @@ exports.up = async function up(knex) {
25
25
  table.integer("owner_user_id").unsigned().notNullable().references("id").inTable("user_profiles").onDelete("CASCADE");
26
26
  table.boolean("is_personal").notNullable().defaultTo(true);
27
27
  table.string("avatar_url", 512).notNullable().defaultTo("");
28
- table.string("color", 7).notNullable().defaultTo("#2F5D9E");
28
+ table.string("color", 7).notNullable().defaultTo("#1867C0");
29
29
  table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
30
30
  table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
31
31
  table.timestamp("deleted_at", { useTz: false }).nullable();
@@ -46,10 +46,14 @@ exports.up = async function up(knex) {
46
46
  table.integer("workspace_id").unsigned().primary().references("id").inTable("workspaces").onDelete("CASCADE");
47
47
  table.string("name", 160).notNullable().defaultTo("Workspace");
48
48
  table.string("avatar_url", 512).notNullable().defaultTo("");
49
- table.string("color", 7).notNullable().defaultTo("#2F5D9E");
50
- table.string("secondary_color", 7).notNullable().defaultTo("#224372");
51
- table.string("surface_color", 7).notNullable().defaultTo("#F0F4F8");
52
- table.string("surface_variant_color", 7).notNullable().defaultTo("#E2E8F1");
49
+ table.string("light_primary_color", 7).notNullable().defaultTo("#1867C0");
50
+ table.string("light_secondary_color", 7).notNullable().defaultTo("#48A9A6");
51
+ table.string("light_surface_color", 7).notNullable().defaultTo("#FFFFFF");
52
+ table.string("light_surface_variant_color", 7).notNullable().defaultTo("#424242");
53
+ table.string("dark_primary_color", 7).notNullable().defaultTo("#2196F3");
54
+ table.string("dark_secondary_color", 7).notNullable().defaultTo("#54B6B2");
55
+ table.string("dark_surface_color", 7).notNullable().defaultTo("#212121");
56
+ table.string("dark_surface_variant_color", 7).notNullable().defaultTo("#C8C8C8");
53
57
  table.boolean("invites_enabled").notNullable().defaultTo(true);
54
58
  table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
55
59
  table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
@@ -4,10 +4,9 @@
4
4
  import { Type } from "typebox";
5
5
  import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
6
6
  import {
7
- coerceWorkspaceColor,
8
- coerceWorkspaceSecondaryColor,
9
- coerceWorkspaceSurfaceColor,
10
- coerceWorkspaceSurfaceVariantColor
7
+ DEFAULT_WORKSPACE_DARK_PALETTE,
8
+ DEFAULT_WORKSPACE_LIGHT_PALETTE,
9
+ coerceWorkspaceThemeColor
11
10
  } from "@jskit-ai/users-core/shared/settings";
12
11
  import {
13
12
  defineField,
@@ -34,10 +33,6 @@ function normalizeHexColor(value) {
34
33
  return /^#[0-9A-Fa-f]{6}$/.test(color) ? color.toUpperCase() : null;
35
34
  }
36
35
 
37
- function resolveThemeBaseColor({ workspace = {}, settings = {} } = {}) {
38
- return normalizeText(settings.color || workspace.color);
39
- }
40
-
41
36
  resetWorkspaceSettingsFields();
42
37
 
43
38
  defineField({
@@ -78,101 +73,163 @@ defineField({
78
73
  });
79
74
 
80
75
  defineField({
81
- key: "color",
82
- dbColumn: "color",
76
+ key: "lightPrimaryColor",
77
+ dbColumn: "light_primary_color",
78
+ required: true,
79
+ inputSchema: Type.String({
80
+ minLength: 7,
81
+ maxLength: 7,
82
+ pattern: "^#[0-9A-Fa-f]{6}$",
83
+ messages: {
84
+ required: "Light primary color is required.",
85
+ pattern: "Light primary color must be a hex color like #1867C0.",
86
+ default: "Light primary color must be a hex color like #1867C0."
87
+ }
88
+ }),
89
+ outputSchema: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" }),
90
+ normalizeInput: normalizeHexColor,
91
+ normalizeOutput: (value) => coerceWorkspaceThemeColor(value, DEFAULT_WORKSPACE_LIGHT_PALETTE.color),
92
+ resolveDefault: () => DEFAULT_WORKSPACE_LIGHT_PALETTE.color
93
+ });
94
+
95
+ defineField({
96
+ key: "lightSecondaryColor",
97
+ dbColumn: "light_secondary_color",
98
+ required: true,
99
+ inputSchema: Type.String({
100
+ minLength: 7,
101
+ maxLength: 7,
102
+ pattern: "^#[0-9A-Fa-f]{6}$",
103
+ messages: {
104
+ required: "Light secondary color is required.",
105
+ pattern: "Light secondary color must be a hex color like #48A9A6.",
106
+ default: "Light secondary color must be a hex color like #48A9A6."
107
+ }
108
+ }),
109
+ outputSchema: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" }),
110
+ normalizeInput: normalizeHexColor,
111
+ normalizeOutput: (value) => coerceWorkspaceThemeColor(value, DEFAULT_WORKSPACE_LIGHT_PALETTE.secondaryColor),
112
+ resolveDefault: () => DEFAULT_WORKSPACE_LIGHT_PALETTE.secondaryColor
113
+ });
114
+
115
+ defineField({
116
+ key: "lightSurfaceColor",
117
+ dbColumn: "light_surface_color",
118
+ required: true,
119
+ inputSchema: Type.String({
120
+ minLength: 7,
121
+ maxLength: 7,
122
+ pattern: "^#[0-9A-Fa-f]{6}$",
123
+ messages: {
124
+ required: "Light surface color is required.",
125
+ pattern: "Light surface color must be a hex color like #FFFFFF.",
126
+ default: "Light surface color must be a hex color like #FFFFFF."
127
+ }
128
+ }),
129
+ outputSchema: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" }),
130
+ normalizeInput: normalizeHexColor,
131
+ normalizeOutput: (value) => coerceWorkspaceThemeColor(value, DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceColor),
132
+ resolveDefault: () => DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceColor
133
+ });
134
+
135
+ defineField({
136
+ key: "lightSurfaceVariantColor",
137
+ dbColumn: "light_surface_variant_color",
138
+ required: true,
139
+ inputSchema: Type.String({
140
+ minLength: 7,
141
+ maxLength: 7,
142
+ pattern: "^#[0-9A-Fa-f]{6}$",
143
+ messages: {
144
+ required: "Light surface variant color is required.",
145
+ pattern: "Light surface variant color must be a hex color like #424242.",
146
+ default: "Light surface variant color must be a hex color like #424242."
147
+ }
148
+ }),
149
+ outputSchema: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" }),
150
+ normalizeInput: normalizeHexColor,
151
+ normalizeOutput: (value) => coerceWorkspaceThemeColor(value, DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceVariantColor),
152
+ resolveDefault: () => DEFAULT_WORKSPACE_LIGHT_PALETTE.surfaceVariantColor
153
+ });
154
+
155
+ defineField({
156
+ key: "darkPrimaryColor",
157
+ dbColumn: "dark_primary_color",
83
158
  required: true,
84
159
  inputSchema: Type.String({
85
160
  minLength: 7,
86
161
  maxLength: 7,
87
162
  pattern: "^#[0-9A-Fa-f]{6}$",
88
163
  messages: {
89
- required: "Workspace color is required.",
90
- pattern: "Workspace color must be a hex color like #2F5D9E.",
91
- default: "Workspace color must be a hex color like #2F5D9E."
164
+ required: "Dark primary color is required.",
165
+ pattern: "Dark primary color must be a hex color like #2196F3.",
166
+ default: "Dark primary color must be a hex color like #2196F3."
92
167
  }
93
168
  }),
94
169
  outputSchema: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" }),
95
170
  normalizeInput: normalizeHexColor,
96
- normalizeOutput: (value) => coerceWorkspaceColor(value),
97
- resolveDefault: ({ workspace = {} } = {}) => coerceWorkspaceColor(workspace.color)
171
+ normalizeOutput: (value) => coerceWorkspaceThemeColor(value, DEFAULT_WORKSPACE_DARK_PALETTE.color),
172
+ resolveDefault: () => DEFAULT_WORKSPACE_DARK_PALETTE.color
98
173
  });
99
174
 
100
175
  defineField({
101
- key: "secondaryColor",
102
- dbColumn: "secondary_color",
176
+ key: "darkSecondaryColor",
177
+ dbColumn: "dark_secondary_color",
103
178
  required: true,
104
179
  inputSchema: Type.String({
105
180
  minLength: 7,
106
181
  maxLength: 7,
107
182
  pattern: "^#[0-9A-Fa-f]{6}$",
108
183
  messages: {
109
- required: "Secondary color is required.",
110
- pattern: "Secondary color must be a hex color like #224372.",
111
- default: "Secondary color must be a hex color like #224372."
184
+ required: "Dark secondary color is required.",
185
+ pattern: "Dark secondary color must be a hex color like #54B6B2.",
186
+ default: "Dark secondary color must be a hex color like #54B6B2."
112
187
  }
113
188
  }),
114
189
  outputSchema: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" }),
115
190
  normalizeInput: normalizeHexColor,
116
- normalizeOutput: (value, { workspace = {}, settings = {} } = {}) =>
117
- coerceWorkspaceSecondaryColor(value, {
118
- color: resolveThemeBaseColor({ workspace, settings })
119
- }),
120
- resolveDefault: ({ workspace = {}, settings = {} } = {}) =>
121
- coerceWorkspaceSecondaryColor(workspace.secondaryColor, {
122
- color: resolveThemeBaseColor({ workspace, settings })
123
- })
191
+ normalizeOutput: (value) => coerceWorkspaceThemeColor(value, DEFAULT_WORKSPACE_DARK_PALETTE.secondaryColor),
192
+ resolveDefault: () => DEFAULT_WORKSPACE_DARK_PALETTE.secondaryColor
124
193
  });
125
194
 
126
195
  defineField({
127
- key: "surfaceColor",
128
- dbColumn: "surface_color",
196
+ key: "darkSurfaceColor",
197
+ dbColumn: "dark_surface_color",
129
198
  required: true,
130
199
  inputSchema: Type.String({
131
200
  minLength: 7,
132
201
  maxLength: 7,
133
202
  pattern: "^#[0-9A-Fa-f]{6}$",
134
203
  messages: {
135
- required: "Surface color is required.",
136
- pattern: "Surface color must be a hex color like #F0F4F8.",
137
- default: "Surface color must be a hex color like #F0F4F8."
204
+ required: "Dark surface color is required.",
205
+ pattern: "Dark surface color must be a hex color like #212121.",
206
+ default: "Dark surface color must be a hex color like #212121."
138
207
  }
139
208
  }),
140
209
  outputSchema: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" }),
141
210
  normalizeInput: normalizeHexColor,
142
- normalizeOutput: (value, { workspace = {}, settings = {} } = {}) =>
143
- coerceWorkspaceSurfaceColor(value, {
144
- color: resolveThemeBaseColor({ workspace, settings })
145
- }),
146
- resolveDefault: ({ workspace = {}, settings = {} } = {}) =>
147
- coerceWorkspaceSurfaceColor(workspace.surfaceColor, {
148
- color: resolveThemeBaseColor({ workspace, settings })
149
- })
211
+ normalizeOutput: (value) => coerceWorkspaceThemeColor(value, DEFAULT_WORKSPACE_DARK_PALETTE.surfaceColor),
212
+ resolveDefault: () => DEFAULT_WORKSPACE_DARK_PALETTE.surfaceColor
150
213
  });
151
214
 
152
215
  defineField({
153
- key: "surfaceVariantColor",
154
- dbColumn: "surface_variant_color",
216
+ key: "darkSurfaceVariantColor",
217
+ dbColumn: "dark_surface_variant_color",
155
218
  required: true,
156
219
  inputSchema: Type.String({
157
220
  minLength: 7,
158
221
  maxLength: 7,
159
222
  pattern: "^#[0-9A-Fa-f]{6}$",
160
223
  messages: {
161
- required: "Surface variant color is required.",
162
- pattern: "Surface variant color must be a hex color like #E2E8F1.",
163
- default: "Surface variant color must be a hex color like #E2E8F1."
224
+ required: "Dark surface variant color is required.",
225
+ pattern: "Dark surface variant color must be a hex color like #C8C8C8.",
226
+ default: "Dark surface variant color must be a hex color like #C8C8C8."
164
227
  }
165
228
  }),
166
229
  outputSchema: Type.String({ minLength: 7, maxLength: 7, pattern: "^#[0-9A-Fa-f]{6}$" }),
167
230
  normalizeInput: normalizeHexColor,
168
- normalizeOutput: (value, { workspace = {}, settings = {} } = {}) =>
169
- coerceWorkspaceSurfaceVariantColor(value, {
170
- color: resolveThemeBaseColor({ workspace, settings })
171
- }),
172
- resolveDefault: ({ workspace = {}, settings = {} } = {}) =>
173
- coerceWorkspaceSurfaceVariantColor(workspace.surfaceVariantColor, {
174
- color: resolveThemeBaseColor({ workspace, settings })
175
- })
231
+ normalizeOutput: (value) => coerceWorkspaceThemeColor(value, DEFAULT_WORKSPACE_DARK_PALETTE.surfaceVariantColor),
232
+ resolveDefault: () => DEFAULT_WORKSPACE_DARK_PALETTE.surfaceVariantColor
176
233
  });
177
234
 
178
235
  defineField({
@@ -1,16 +1,14 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import "../test-support/registerDefaultSettingsFields.js";
4
- import { resolveWorkspaceThemePalette } from "../src/shared/settings.js";
4
+ import { resolveWorkspaceThemePalettes } from "../src/shared/settings.js";
5
5
  import { createRepository } from "../src/server/workspaceSettings/workspaceSettingsRepository.js";
6
6
 
7
7
  function createDefaultWorkspaceSettings() {
8
8
  return true;
9
9
  }
10
10
 
11
- const DEFAULT_WORKSPACE_THEME = resolveWorkspaceThemePalette({
12
- color: "#2F5D9E"
13
- });
11
+ const DEFAULT_WORKSPACE_THEME = resolveWorkspaceThemePalettes({});
14
12
 
15
13
  function createKnexStub(rowOverrides = {}) {
16
14
  const state = {
@@ -20,10 +18,14 @@ function createKnexStub(rowOverrides = {}) {
20
18
  workspace_id: 1,
21
19
  name: "Workspace",
22
20
  avatar_url: "",
23
- color: "#2F5D9E",
24
- secondary_color: DEFAULT_WORKSPACE_THEME.secondaryColor,
25
- surface_color: DEFAULT_WORKSPACE_THEME.surfaceColor,
26
- surface_variant_color: DEFAULT_WORKSPACE_THEME.surfaceVariantColor,
21
+ light_primary_color: DEFAULT_WORKSPACE_THEME.light.color,
22
+ light_secondary_color: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
23
+ light_surface_color: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
24
+ light_surface_variant_color: DEFAULT_WORKSPACE_THEME.light.surfaceVariantColor,
25
+ dark_primary_color: DEFAULT_WORKSPACE_THEME.dark.color,
26
+ dark_secondary_color: DEFAULT_WORKSPACE_THEME.dark.secondaryColor,
27
+ dark_surface_color: DEFAULT_WORKSPACE_THEME.dark.surfaceColor,
28
+ dark_surface_variant_color: DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor,
27
29
  invites_enabled: 1,
28
30
  created_at: "2026-03-09 00:26:35.710",
29
31
  updated_at: "2026-03-09 00:26:35.710",
@@ -41,10 +43,14 @@ function createKnexStub(rowOverrides = {}) {
41
43
  workspace_id: payload.workspace_id,
42
44
  name: payload.name,
43
45
  avatar_url: payload.avatar_url,
44
- color: payload.color,
45
- secondary_color: payload.secondary_color,
46
- surface_color: payload.surface_color,
47
- surface_variant_color: payload.surface_variant_color,
46
+ light_primary_color: payload.light_primary_color,
47
+ light_secondary_color: payload.light_secondary_color,
48
+ light_surface_color: payload.light_surface_color,
49
+ light_surface_variant_color: payload.light_surface_variant_color,
50
+ dark_primary_color: payload.dark_primary_color,
51
+ dark_secondary_color: payload.dark_secondary_color,
52
+ dark_surface_color: payload.dark_surface_color,
53
+ dark_surface_variant_color: payload.dark_surface_variant_color,
48
54
  invites_enabled: payload.invites_enabled,
49
55
  created_at: "2026-03-10 00:00:00.000",
50
56
  updated_at: "2026-03-10 00:00:00.000"
@@ -69,17 +75,29 @@ function createKnexStub(rowOverrides = {}) {
69
75
  if (Object.hasOwn(payload, "avatar_url")) {
70
76
  state.row.avatar_url = payload.avatar_url;
71
77
  }
72
- if (Object.hasOwn(payload, "color")) {
73
- state.row.color = payload.color;
78
+ if (Object.hasOwn(payload, "light_primary_color")) {
79
+ state.row.light_primary_color = payload.light_primary_color;
80
+ }
81
+ if (Object.hasOwn(payload, "light_secondary_color")) {
82
+ state.row.light_secondary_color = payload.light_secondary_color;
83
+ }
84
+ if (Object.hasOwn(payload, "light_surface_color")) {
85
+ state.row.light_surface_color = payload.light_surface_color;
86
+ }
87
+ if (Object.hasOwn(payload, "light_surface_variant_color")) {
88
+ state.row.light_surface_variant_color = payload.light_surface_variant_color;
89
+ }
90
+ if (Object.hasOwn(payload, "dark_primary_color")) {
91
+ state.row.dark_primary_color = payload.dark_primary_color;
74
92
  }
75
- if (Object.hasOwn(payload, "secondary_color")) {
76
- state.row.secondary_color = payload.secondary_color;
93
+ if (Object.hasOwn(payload, "dark_secondary_color")) {
94
+ state.row.dark_secondary_color = payload.dark_secondary_color;
77
95
  }
78
- if (Object.hasOwn(payload, "surface_color")) {
79
- state.row.surface_color = payload.surface_color;
96
+ if (Object.hasOwn(payload, "dark_surface_color")) {
97
+ state.row.dark_surface_color = payload.dark_surface_color;
80
98
  }
81
- if (Object.hasOwn(payload, "surface_variant_color")) {
82
- state.row.surface_variant_color = payload.surface_variant_color;
99
+ if (Object.hasOwn(payload, "dark_surface_variant_color")) {
100
+ state.row.dark_surface_variant_color = payload.dark_surface_variant_color;
83
101
  }
84
102
  if (Object.hasOwn(payload, "updated_at")) {
85
103
  state.row.updated_at = payload.updated_at;
@@ -106,10 +124,14 @@ test("workspaceSettingsRepository.findByWorkspaceId maps the stored row", async
106
124
  workspaceId: 1,
107
125
  name: "Workspace",
108
126
  avatarUrl: "",
109
- color: "#2F5D9E",
110
- secondaryColor: DEFAULT_WORKSPACE_THEME.secondaryColor,
111
- surfaceColor: DEFAULT_WORKSPACE_THEME.surfaceColor,
112
- surfaceVariantColor: DEFAULT_WORKSPACE_THEME.surfaceVariantColor,
127
+ lightPrimaryColor: DEFAULT_WORKSPACE_THEME.light.color,
128
+ lightSecondaryColor: DEFAULT_WORKSPACE_THEME.light.secondaryColor,
129
+ lightSurfaceColor: DEFAULT_WORKSPACE_THEME.light.surfaceColor,
130
+ lightSurfaceVariantColor: DEFAULT_WORKSPACE_THEME.light.surfaceVariantColor,
131
+ darkPrimaryColor: DEFAULT_WORKSPACE_THEME.dark.color,
132
+ darkSecondaryColor: DEFAULT_WORKSPACE_THEME.dark.secondaryColor,
133
+ darkSurfaceColor: DEFAULT_WORKSPACE_THEME.dark.surfaceColor,
134
+ darkSurfaceVariantColor: DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor,
113
135
  invitesEnabled: true,
114
136
  createdAt: "2026-03-08T16:26:35.710Z",
115
137
  updatedAt: "2026-03-08T16:26:35.710Z"
@@ -142,17 +164,31 @@ test("workspaceSettingsRepository.ensureForWorkspaceId inserts the injected defa
142
164
  assert.equal(state.insertedRow.workspace_id, 5);
143
165
  assert.equal(state.insertedRow.name, "Workspace");
144
166
  assert.equal(state.insertedRow.avatar_url, "");
145
- assert.equal(state.insertedRow.color, "#2F5D9E");
146
- assert.equal(state.insertedRow.secondary_color, DEFAULT_WORKSPACE_THEME.secondaryColor);
147
- assert.equal(state.insertedRow.surface_color, DEFAULT_WORKSPACE_THEME.surfaceColor);
148
- assert.equal(state.insertedRow.surface_variant_color, DEFAULT_WORKSPACE_THEME.surfaceVariantColor);
167
+ assert.equal(state.insertedRow.light_primary_color, DEFAULT_WORKSPACE_THEME.light.color);
168
+ assert.equal(state.insertedRow.light_secondary_color, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
169
+ assert.equal(state.insertedRow.light_surface_color, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
170
+ assert.equal(
171
+ state.insertedRow.light_surface_variant_color,
172
+ DEFAULT_WORKSPACE_THEME.light.surfaceVariantColor
173
+ );
174
+ assert.equal(state.insertedRow.dark_primary_color, DEFAULT_WORKSPACE_THEME.dark.color);
175
+ assert.equal(state.insertedRow.dark_secondary_color, DEFAULT_WORKSPACE_THEME.dark.secondaryColor);
176
+ assert.equal(state.insertedRow.dark_surface_color, DEFAULT_WORKSPACE_THEME.dark.surfaceColor);
177
+ assert.equal(
178
+ state.insertedRow.dark_surface_variant_color,
179
+ DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor
180
+ );
149
181
  assert.equal(state.insertedRow.invites_enabled, false);
150
182
  assert.equal(record.name, "Workspace");
151
183
  assert.equal(record.avatarUrl, "");
152
- assert.equal(record.color, "#2F5D9E");
153
- assert.equal(record.secondaryColor, DEFAULT_WORKSPACE_THEME.secondaryColor);
154
- assert.equal(record.surfaceColor, DEFAULT_WORKSPACE_THEME.surfaceColor);
155
- assert.equal(record.surfaceVariantColor, DEFAULT_WORKSPACE_THEME.surfaceVariantColor);
184
+ assert.equal(record.lightPrimaryColor, DEFAULT_WORKSPACE_THEME.light.color);
185
+ assert.equal(record.lightSecondaryColor, DEFAULT_WORKSPACE_THEME.light.secondaryColor);
186
+ assert.equal(record.lightSurfaceColor, DEFAULT_WORKSPACE_THEME.light.surfaceColor);
187
+ assert.equal(record.lightSurfaceVariantColor, DEFAULT_WORKSPACE_THEME.light.surfaceVariantColor);
188
+ assert.equal(record.darkPrimaryColor, DEFAULT_WORKSPACE_THEME.dark.color);
189
+ assert.equal(record.darkSecondaryColor, DEFAULT_WORKSPACE_THEME.dark.secondaryColor);
190
+ assert.equal(record.darkSurfaceColor, DEFAULT_WORKSPACE_THEME.dark.surfaceColor);
191
+ assert.equal(record.darkSurfaceVariantColor, DEFAULT_WORKSPACE_THEME.dark.surfaceVariantColor);
156
192
  assert.equal(record.invitesEnabled, false);
157
193
  });
158
194
 
@@ -165,15 +201,15 @@ test("workspaceSettingsRepository.updateSettingsByWorkspaceId updates name/avata
165
201
  const updated = await repository.updateSettingsByWorkspaceId(1, {
166
202
  name: "New name",
167
203
  avatarUrl: "https://example.com/avatar.png",
168
- color: "#123abc"
204
+ lightPrimaryColor: "#123abc"
169
205
  });
170
206
 
171
207
  assert.equal(state.updatePayload.name, "New name");
172
208
  assert.equal(state.updatePayload.avatar_url, "https://example.com/avatar.png");
173
- assert.equal(state.updatePayload.color, "#123ABC");
209
+ assert.equal(state.updatePayload.light_primary_color, "#123ABC");
174
210
  assert.equal(updated.name, "New name");
175
211
  assert.equal(updated.avatarUrl, "https://example.com/avatar.png");
176
- assert.equal(updated.color, "#123ABC");
212
+ assert.equal(updated.lightPrimaryColor, "#123ABC");
177
213
  });
178
214
 
179
215
  test("workspaceSettingsRepository can be constructed without validating app config shape", () => {
@@ -2,7 +2,7 @@ import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
4
4
  import "../test-support/registerDefaultSettingsFields.js";
5
- import { resolveWorkspaceThemePalette } from "../src/shared/settings.js";
5
+ import { resolveWorkspaceThemePalettes } from "../src/shared/settings.js";
6
6
  import { workspaceSettingsResource } from "../src/shared/resources/workspaceSettingsResource.js";
7
7
  import { createWorkspaceRoleCatalog } from "../src/shared/roles.js";
8
8
 
@@ -48,10 +48,14 @@ test("workspace settings patch body normalizes valid payload before validation",
48
48
  const parsed = parseBody(workspaceSettingsResource.operations.patch, {
49
49
  name: " Team Mercury ",
50
50
  avatarUrl: "https://example.com/avatar.png",
51
- color: "#0f6b54",
52
- secondaryColor: "#0b4d3c",
53
- surfaceColor: "#eef5f3",
54
- surfaceVariantColor: "#ddeae7",
51
+ lightPrimaryColor: "#0f6b54",
52
+ lightSecondaryColor: "#0b4d3c",
53
+ lightSurfaceColor: "#eef5f3",
54
+ lightSurfaceVariantColor: "#ddeae7",
55
+ darkPrimaryColor: "#123456",
56
+ darkSecondaryColor: "#234567",
57
+ darkSurfaceColor: "#345678",
58
+ darkSurfaceVariantColor: "#456789",
55
59
  invitesEnabled: false
56
60
  });
57
61
 
@@ -60,10 +64,14 @@ test("workspace settings patch body normalizes valid payload before validation",
60
64
  assert.deepEqual(parsed.value, {
61
65
  name: "Team Mercury",
62
66
  avatarUrl: "https://example.com/avatar.png",
63
- color: "#0F6B54",
64
- secondaryColor: "#0B4D3C",
65
- surfaceColor: "#EEF5F3",
66
- surfaceVariantColor: "#DDEAE7",
67
+ lightPrimaryColor: "#0F6B54",
68
+ lightSecondaryColor: "#0B4D3C",
69
+ lightSurfaceColor: "#EEF5F3",
70
+ lightSurfaceVariantColor: "#DDEAE7",
71
+ darkPrimaryColor: "#123456",
72
+ darkSecondaryColor: "#234567",
73
+ darkSurfaceColor: "#345678",
74
+ darkSurfaceVariantColor: "#456789",
67
75
  invitesEnabled: false
68
76
  });
69
77
  });
@@ -95,16 +103,20 @@ test("workspace settings create body requires full-write fields", () => {
95
103
  });
96
104
 
97
105
  assert.equal(parsed.ok, false);
98
- assert.equal(parsed.fieldErrors.color, "Workspace color is required.");
99
- assert.equal(parsed.fieldErrors.secondaryColor, "Secondary color is required.");
100
- assert.equal(parsed.fieldErrors.surfaceColor, "Surface color is required.");
101
- assert.equal(parsed.fieldErrors.surfaceVariantColor, "Surface variant color is required.");
106
+ assert.equal(parsed.fieldErrors.lightPrimaryColor, "Light primary color is required.");
107
+ assert.equal(parsed.fieldErrors.lightSecondaryColor, "Light secondary color is required.");
108
+ assert.equal(parsed.fieldErrors.lightSurfaceColor, "Light surface color is required.");
109
+ assert.equal(parsed.fieldErrors.lightSurfaceVariantColor, "Light surface variant color is required.");
110
+ assert.equal(parsed.fieldErrors.darkPrimaryColor, "Dark primary color is required.");
111
+ assert.equal(parsed.fieldErrors.darkSecondaryColor, "Dark secondary color is required.");
112
+ assert.equal(parsed.fieldErrors.darkSurfaceColor, "Dark surface color is required.");
113
+ assert.equal(parsed.fieldErrors.darkSurfaceVariantColor, "Dark surface variant color is required.");
102
114
  assert.equal(parsed.fieldErrors.invitesEnabled, "invitesEnabled is required.");
103
115
  });
104
116
 
105
117
  test("workspace settings output normalizes raw service payloads", () => {
106
- const expectedTheme = resolveWorkspaceThemePalette({
107
- color: "#0F6B54"
118
+ const expectedTheme = resolveWorkspaceThemePalettes({
119
+ lightPrimaryColor: "#0F6B54"
108
120
  });
109
121
  const normalized = workspaceSettingsResource.operations.view.outputValidator.normalize({
110
122
  workspace: {
@@ -115,7 +127,7 @@ test("workspace settings output normalizes raw service payloads", () => {
115
127
  settings: {
116
128
  name: " Mercury Workspace ",
117
129
  avatarUrl: " https://example.com/avatar.png ",
118
- color: "#0f6b54",
130
+ lightPrimaryColor: "#0f6b54",
119
131
  invitesEnabled: false
120
132
  },
121
133
  roleCatalog: createRoleCatalog()
@@ -130,10 +142,14 @@ test("workspace settings output normalizes raw service payloads", () => {
130
142
  settings: {
131
143
  name: "Mercury Workspace",
132
144
  avatarUrl: "https://example.com/avatar.png",
133
- color: "#0F6B54",
134
- secondaryColor: expectedTheme.secondaryColor,
135
- surfaceColor: expectedTheme.surfaceColor,
136
- surfaceVariantColor: expectedTheme.surfaceVariantColor,
145
+ lightPrimaryColor: "#0F6B54",
146
+ lightSecondaryColor: expectedTheme.light.secondaryColor,
147
+ lightSurfaceColor: expectedTheme.light.surfaceColor,
148
+ lightSurfaceVariantColor: expectedTheme.light.surfaceVariantColor,
149
+ darkPrimaryColor: expectedTheme.dark.color,
150
+ darkSecondaryColor: expectedTheme.dark.secondaryColor,
151
+ darkSurfaceColor: expectedTheme.dark.surfaceColor,
152
+ darkSurfaceVariantColor: expectedTheme.dark.surfaceVariantColor,
137
153
  invitesEnabled: false,
138
154
  invitesAvailable: true,
139
155
  invitesEffective: false
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import "../test-support/registerDefaultSettingsFields.js";
4
- import { resolveWorkspaceThemePalette } from "../src/shared/settings.js";
4
+ import { resolveWorkspaceThemePalettes } from "../src/shared/settings.js";
5
5
  import { createService } from "../src/server/workspaceSettings/workspaceSettingsService.js";
6
6
 
7
7
  function authorizedOptions(permissions = []) {
@@ -16,8 +16,8 @@ function authorizedOptions(permissions = []) {
16
16
  }
17
17
 
18
18
  function createFixture({ workspaceInvitationsEnabled = true } = {}) {
19
- const defaultTheme = resolveWorkspaceThemePalette({
20
- color: "#0F6B54"
19
+ const defaultTheme = resolveWorkspaceThemePalettes({
20
+ lightPrimaryColor: "#0F6B54"
21
21
  });
22
22
  const state = {
23
23
  settingsPatch: null,
@@ -27,15 +27,19 @@ function createFixture({ workspaceInvitationsEnabled = true } = {}) {
27
27
  name: "TonyMobily3",
28
28
  ownerUserId: 9,
29
29
  avatarUrl: "",
30
- color: defaultTheme.color
30
+ color: defaultTheme.light.color
31
31
  },
32
32
  settings: {
33
33
  name: "TonyMobily3",
34
34
  avatarUrl: "",
35
- color: defaultTheme.color,
36
- secondaryColor: defaultTheme.secondaryColor,
37
- surfaceColor: defaultTheme.surfaceColor,
38
- surfaceVariantColor: defaultTheme.surfaceVariantColor,
35
+ lightPrimaryColor: defaultTheme.light.color,
36
+ lightSecondaryColor: defaultTheme.light.secondaryColor,
37
+ lightSurfaceColor: defaultTheme.light.surfaceColor,
38
+ lightSurfaceVariantColor: defaultTheme.light.surfaceVariantColor,
39
+ darkPrimaryColor: defaultTheme.dark.color,
40
+ darkSecondaryColor: defaultTheme.dark.secondaryColor,
41
+ darkSurfaceColor: defaultTheme.dark.surfaceColor,
42
+ darkSurfaceVariantColor: defaultTheme.dark.surfaceVariantColor,
39
43
  invitesEnabled: true
40
44
  }
41
45
  };
@@ -73,10 +77,14 @@ test("workspaceSettingsService.getWorkspaceSettings returns the stored invitesEn
73
77
  assert.deepEqual(response.settings, {
74
78
  name: "TonyMobily3",
75
79
  avatarUrl: "",
76
- color: "#0F6B54",
77
- secondaryColor: "#0B4D3C",
78
- surfaceColor: "#EEF5F3",
79
- surfaceVariantColor: "#DDEAE7",
80
+ lightPrimaryColor: "#0F6B54",
81
+ lightSecondaryColor: "#48A9A6",
82
+ lightSurfaceColor: "#FFFFFF",
83
+ lightSurfaceVariantColor: "#424242",
84
+ darkPrimaryColor: "#2196F3",
85
+ darkSecondaryColor: "#54B6B2",
86
+ darkSurfaceColor: "#212121",
87
+ darkSurfaceVariantColor: "#C8C8C8",
80
88
  invitesEnabled: true,
81
89
  invitesAvailable: true,
82
90
  invitesEffective: true
@@ -102,10 +110,14 @@ test("workspaceSettingsService.updateWorkspaceSettings writes editable fields th
102
110
  assert.deepEqual(response.settings, {
103
111
  name: "New Name",
104
112
  avatarUrl: "",
105
- color: "#0F6B54",
106
- secondaryColor: "#0B4D3C",
107
- surfaceColor: "#EEF5F3",
108
- surfaceVariantColor: "#DDEAE7",
113
+ lightPrimaryColor: "#0F6B54",
114
+ lightSecondaryColor: "#48A9A6",
115
+ lightSurfaceColor: "#FFFFFF",
116
+ lightSurfaceVariantColor: "#424242",
117
+ darkPrimaryColor: "#2196F3",
118
+ darkSecondaryColor: "#54B6B2",
119
+ darkSurfaceColor: "#212121",
120
+ darkSurfaceVariantColor: "#C8C8C8",
109
121
  invitesEnabled: false,
110
122
  invitesAvailable: true,
111
123
  invitesEffective: false
@@ -125,10 +137,14 @@ test("workspaceSettingsService disables invite settings in output when app polic
125
137
  assert.deepEqual(response.settings, {
126
138
  name: "TonyMobily3",
127
139
  avatarUrl: "",
128
- color: "#0F6B54",
129
- secondaryColor: "#0B4D3C",
130
- surfaceColor: "#EEF5F3",
131
- surfaceVariantColor: "#DDEAE7",
140
+ lightPrimaryColor: "#0F6B54",
141
+ lightSecondaryColor: "#48A9A6",
142
+ lightSurfaceColor: "#FFFFFF",
143
+ lightSurfaceVariantColor: "#424242",
144
+ darkPrimaryColor: "#2196F3",
145
+ darkSecondaryColor: "#54B6B2",
146
+ darkSurfaceColor: "#212121",
147
+ darkSurfaceVariantColor: "#C8C8C8",
132
148
  invitesEnabled: false,
133
149
  invitesAvailable: false,
134
150
  invitesEffective: false