diff --git a/packages/ui/src/components/WorkspaceAvatar.svelte b/packages/ui/src/components/WorkspaceAvatar.svelte
new file mode 100644
index 0000000000..f02292c19c
--- /dev/null
+++ b/packages/ui/src/components/WorkspaceAvatar.svelte
@@ -0,0 +1,79 @@
+
+
+
+
+
+ {getWorkspaceInitial(displayName)}
+
+ {#if hasUnread}
+
+ {/if}
+
+
+
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 53d92fca99..afa70dc96f 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -67,6 +67,7 @@ export { default as StatusBadge } from './components/StatusBadge.svelte'
export { default as StateTag } from './components/StateTag.svelte'
export { default as Component } from './components/Component.svelte'
export { default as Icon } from './components/Icon.svelte'
+export { default as WorkspaceAvatar } from './components/WorkspaceAvatar.svelte'
export { default as ActionIcon } from './components/ActionIcon.svelte'
export { default as Toggle } from './components/Toggle.svelte'
export { default as RadioButton } from './components/RadioButton.svelte'
@@ -307,6 +308,7 @@ export * from './tooltips'
export * from './panelup'
export * from './components/calendar/internal/DateUtils'
export * from './colors'
+export * from './workspace'
export * from './focus'
export * from './resize'
export * from './lazy'
diff --git a/packages/ui/src/workspace.ts b/packages/ui/src/workspace.ts
new file mode 100644
index 0000000000..76b67a5ed9
--- /dev/null
+++ b/packages/ui/src/workspace.ts
@@ -0,0 +1,35 @@
+//
+// Copyright © 2026 TraceX SAS.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+/**
+ * Shared helpers for rendering workspace selection UI (login page, in-app
+ * workspace switcher). Kept here so both places stay visually consistent
+ * without introducing a dependency between the plugins that own them.
+ * @public
+ */
+export function getWorkspaceInitial (name: string | undefined): string {
+ const initial = (name ?? '').trim().charAt(0).toUpperCase()
+ return initial === '' ? '?' : initial
+}
+
+/**
+ * @public
+ */
+export function getWorkspaceLastVisitDays (lastVisit: number | undefined): number | undefined {
+ if (lastVisit === undefined || lastVisit === 0) {
+ return undefined
+ }
+ return Math.round((Date.now() - lastVisit) / (1000 * 3600 * 24))
+}
diff --git a/plugins/login-resources/src/components/SelectWorkspace.svelte b/plugins/login-resources/src/components/SelectWorkspace.svelte
index 9060e6b6ed..8c7b89c812 100644
--- a/plugins/login-resources/src/components/SelectWorkspace.svelte
+++ b/plugins/login-resources/src/components/SelectWorkspace.svelte
@@ -23,14 +23,16 @@
} from '@hcengineering/core'
import { LoginInfo } from '@hcengineering/login'
import { OK, Severity, Status } from '@hcengineering/platform'
- import presentation, { MessageBox, NavLink, isAdminUser, reduceCalls } from '@hcengineering/presentation'
+ import presentation, { MessageBox, NavLink, reduceCalls } from '@hcengineering/presentation'
import {
Button,
Label,
Scroller,
SearchEdit,
Spinner,
+ WorkspaceAvatar,
deviceOptionsStore as deviceInfo,
+ getWorkspaceLastVisitDays,
showPopup,
ticker
} from '@hcengineering/ui'
@@ -53,22 +55,6 @@
export let navigateUrl: string | undefined = undefined
- // Workspaces have no icon/logo of their own, so we derive a stable
- // colored initial as a lightweight stand-in for a real workspace icon.
- const workspaceAvatarColors = ['#5B9BF0', '#63BC8B', '#E0A64D', '#D96B6B', '#9B7FE0', '#4DB6C7', '#E08FC0', '#8CC152']
-
- function workspaceAvatarColor (id: string): string {
- let hash = 0
- for (let i = 0; i < id.length; i++) {
- hash = (hash * 31 + id.charCodeAt(i)) | 0
- }
- return workspaceAvatarColors[Math.abs(hash) % workspaceAvatarColors.length]
- }
-
- function workspaceInitial (name: string): string {
- return name.trim().charAt(0).toUpperCase() || '?'
- }
-
let workspaces: WorkspaceInfoWithStatus[] = []
let status = OK
let accountPromise: Promise
@@ -186,21 +172,19 @@
.filter((it) => search === '' || (it.name?.includes(search) ?? false) || it.url.includes(search))
.slice(0, 500) as workspace}
{@const wsName = workspace.name ?? workspace.url}
- {@const lastUsageDays =
- workspace.lastVisit === undefined
- ? 'N/A'
- : Math.round((Date.now() - workspace.lastVisit) / (1000 * 3600 * 24))}
+ {@const lastUsageDays = getWorkspaceLastVisitDays(workspace.lastVisit)}
{/each}
@@ -256,14 +240,6 @@
diff --git a/plugins/workbench-resources/src/plugin.ts b/plugins/workbench-resources/src/plugin.ts
index 7d23bead95..3162a3305b 100644
--- a/plugins/workbench-resources/src/plugin.ts
+++ b/plugins/workbench-resources/src/plugin.ts
@@ -46,7 +46,9 @@ export default mergeIds(workbenchId, workbench, {
AccessDenied: '' as IntlString,
Widget: '' as IntlString,
WidgetPreference: '' as IntlString,
- Tab: '' as IntlString
+ Tab: '' as IntlString,
+ FailedToLoadWorkspaces: '' as IntlString,
+ NoWorkspacesFound: '' as IntlString
},
component: {
SpacePanel: '' as AnyComponent,