Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions packages/ui/src/components/WorkspaceAvatar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!--
// 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.
-->
<script lang="ts">
import { themeStore } from '@hcengineering/theme'

import { getPlatformColorForText } from '../colors'
import { getWorkspaceInitial } from '../workspace'

export let colorSeed: string
export let displayName: string
export let size: 'small' | 'medium' = 'small'
export let hasUnread: boolean = false
// Color of the surface the avatar sits on, so the unread ring stays
// visible instead of blending into the avatar itself.
export let ringColor: string = 'var(--theme-popup-color)'

$: color = getPlatformColorForText(colorSeed, $themeStore.dark)
</script>

<div class="workspaceAvatar-wrap {size}">
<div class="workspaceAvatar-circle" style:background-color={color}>
{getWorkspaceInitial(displayName)}
</div>
{#if hasUnread}
<div class="workspaceAvatar-unread" style:box-shadow={`0 0 0 0.125rem ${ringColor}`} />
{/if}
</div>

<style lang="scss">
.workspaceAvatar-wrap {
position: relative;
display: flex;
flex-shrink: 0;

&.small {
width: 1.75rem;
height: 1.75rem;
}
&.medium {
width: 2rem;
height: 2rem;
}
}
.workspaceAvatar-circle {
width: 100%;
height: 100%;
// Rounded square, matching the sidebar workspace logo (Logo.svelte)
// instead of a plain circle.
border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
color: #fff;
}
.workspaceAvatar-unread {
position: absolute;
top: -0.0625rem;
right: -0.0625rem;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: var(--global-higlight-Color);
pointer-events: none;
}
</style>
2 changes: 2 additions & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
35 changes: 35 additions & 0 deletions packages/ui/src/workspace.ts
Original file line number Diff line number Diff line change
@@ -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))
}
61 changes: 12 additions & 49 deletions plugins/login-resources/src/components/SelectWorkspace.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<LoginInfo | null>
Expand Down Expand Up @@ -186,29 +172,27 @@
.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)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="workspace cursor-pointer focused-button bordered form-row" on:click={() => select(workspace.url)}>
<div class="workspace-icon" style:background={workspaceAvatarColor(workspace.uuid)}>
{workspaceInitial(wsName)}
</div>
<WorkspaceAvatar
colorSeed={workspace.uuid}
displayName={wsName}
size={'small'}
hasUnread={workspace.hasUnread === true}
ringColor={'var(--theme-bg-color)'}
/>
<span class="workspace-name overflow-label">
{wsName}
{#if workspace.hasUnread === true}
<div class="unread-marker" />
{/if}
{#if isArchivingMode(workspace.mode)}
- <Label label={presentation.string.Archived} />
{/if}
{#if !isActiveMode(workspace.mode) && !isArchivingMode(workspace.mode)}
({workspace.processingProgress}%)
{/if}
</span>
<span class="workspace-meta">{lastUsageDays} d</span>
<span class="workspace-meta">{lastUsageDays === undefined ? 'N/A' : `${lastUsageDays} d`}</span>
</div>
{/each}

Expand Down Expand Up @@ -256,14 +240,6 @@
</form>

<style lang="scss">
.unread-marker {
flex-shrink: 0;
margin-left: 0.375rem;
width: 0.375rem;
height: 0.375rem;
border-radius: 50%;
background-color: var(--global-higlight-Color);
}
.container {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -310,19 +286,6 @@
text-align: left;
}

.workspace-icon {
flex-shrink: 0;
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
color: #fff;
}

.workspace-name {
flex: 1;
min-width: 0;
Expand Down
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "Nastavit widgety",
"Tab": "Karta",
"WorkspaceIsArchived": "Pracovní prostor je archivován kvůli nečinnosti. Kontaktujte nás prosím pro obnovení...",
"WorkspaceIsMigrating": "Pracovní prostor je aktualizován. Prosím čekejte..."
"WorkspaceIsMigrating": "Pracovní prostor je aktualizován. Prosím čekejte...",
"FailedToLoadWorkspaces": "Nepodařilo se načíst pracovní prostory",
"NoWorkspacesFound": "Nebyly nalezeny žádné pracovní prostory"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "Widgets konfigurieren",
"Tab": "Tab",
"WorkspaceIsArchived": "Workspace wurde wegen Inaktivität archiviert. Bitte kontaktieren Sie uns zur Wiederherstellung...",
"WorkspaceIsMigrating": "Workspace wird aktualisiert. Bitte warten..."
"WorkspaceIsMigrating": "Workspace wird aktualisiert. Bitte warten...",
"FailedToLoadWorkspaces": "Workspaces konnten nicht geladen werden",
"NoWorkspacesFound": "Keine Workspaces gefunden"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "Configure widgets",
"Tab": "Tab",
"WorkspaceIsArchived": "Workspace is archived because of being unused, Please contact us to restore...",
"WorkspaceIsMigrating": "Workspace is being updated. Please wait..."
"WorkspaceIsMigrating": "Workspace is being updated. Please wait...",
"FailedToLoadWorkspaces": "Failed to load workspaces",
"NoWorkspacesFound": "No workspaces found"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "Configurar widgets",
"Tab": "Pestaña",
"WorkspaceIsArchived": "El espacio de trabajo está archivado por no estar en uso, por favor contáctenos para restaurarlo...",
"WorkspaceIsMigrating": "El espacio de trabajo se está actualizando. Por favor, espere..."
"WorkspaceIsMigrating": "El espacio de trabajo se está actualizando. Por favor, espere...",
"FailedToLoadWorkspaces": "No se pudieron cargar los espacios de trabajo",
"NoWorkspacesFound": "No se encontraron espacios de trabajo"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "Configurer les widgets",
"Tab": "Onglet",
"WorkspaceIsArchived": "L'espace de travail est archivé en raison de son inactivité, veuillez nous contacter pour le restaurer...",
"WorkspaceIsMigrating": "L'espace de travail est en cours de mise à jour. Veuillez patienter..."
"WorkspaceIsMigrating": "L'espace de travail est en cours de mise à jour. Veuillez patienter...",
"FailedToLoadWorkspaces": "Échec du chargement des espaces de travail",
"NoWorkspacesFound": "Aucun espace de travail trouvé"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"ServerUnderMaintenance": "Il server è in manutenzione",
"WorkspaceCreating": "Creazione in corso...",
"AccessDenied": "L'oggetto non esiste o non hai autorizzazione per accedervi.",
"WorkspaceIsMigrating": "Il workspace è in fase di aggiornamento. Attendi..."
"WorkspaceIsMigrating": "Il workspace è in fase di aggiornamento. Attendi...",
"FailedToLoadWorkspaces": "Impossibile caricare gli spazi di lavoro",
"NoWorkspacesFound": "Nessuno spazio di lavoro trovato"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "ウィジェットを設定",
"Tab": "タブ",
"WorkspaceIsArchived": "ワークスペースは未使用のためアーカイブされています。復元するにはお問い合わせください...",
"WorkspaceIsMigrating": "ワークスペースを更新中です。しばらくお待ちください..."
"WorkspaceIsMigrating": "ワークスペースを更新中です。しばらくお待ちください...",
"FailedToLoadWorkspaces": "ワークスペースの読み込みに失敗しました",
"NoWorkspacesFound": "ワークスペースが見つかりません"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "위젯 구성",
"Tab": "탭",
"WorkspaceIsArchived": "사용하지 않아 보관된 워크스페이스입니다. 복원하려면 문의해 주세요...",
"WorkspaceIsMigrating": "워크스페이스 업데이트 중입니다. 잠시만 기다려 주세요..."
"WorkspaceIsMigrating": "워크스페이스 업데이트 중입니다. 잠시만 기다려 주세요...",
"FailedToLoadWorkspaces": "워크스페이스를 불러오지 못했습니다",
"NoWorkspacesFound": "워크스페이스를 찾을 수 없습니다"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "Konfiguruj widżety",
"Tab": "Karta",
"WorkspaceIsArchived": "Przestrzeń robocza jest zarchiwizowana z powodu nieaktywności. Skontaktuj się z nami, żeby ją przywrócić...",
"WorkspaceIsMigrating": "Trwa aktualizacja przestrzeni roboczej. Proszę czekać..."
"WorkspaceIsMigrating": "Trwa aktualizacja przestrzeni roboczej. Proszę czekać...",
"FailedToLoadWorkspaces": "Nie udało się wczytać przestrzeni roboczych",
"NoWorkspacesFound": "Nie znaleziono przestrzeni roboczych"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "Configurar widgets",
"Tab": "Aba",
"WorkspaceIsArchived": "O espaço de trabalho está arquivado por estar inativo, por favor, entre em contato conosco para restaurá-lo...",
"WorkspaceIsMigrating": "O espaço de trabalho está sendo atualizado. Por favor, aguarde..."
"WorkspaceIsMigrating": "O espaço de trabalho está sendo atualizado. Por favor, aguarde...",
"FailedToLoadWorkspaces": "Falha ao carregar os espaços de trabalho",
"NoWorkspacesFound": "Nenhum espaço de trabalho encontrado"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "Configurar widgets",
"Tab": "Aba",
"WorkspaceIsArchived": "O espaço de trabalho está arquivado por estar inativo, por favor, entre em contato conosco para restaurá-lo...",
"WorkspaceIsMigrating": "O espaço de trabalho está sendo atualizado. Por favor, aguarde..."
"WorkspaceIsMigrating": "O espaço de trabalho está sendo atualizado. Por favor, aguarde...",
"FailedToLoadWorkspaces": "Falha ao carregar os espaços de trabalho",
"NoWorkspacesFound": "Nenhum espaço de trabalho encontrado"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "Настроить виджеты",
"Tab": "Вкладка",
"WorkspaceIsArchived": "Рабочее пространство архивировано из-за неиспользования, пожалуйста, свяжитесь с нами для восстановления...",
"WorkspaceIsMigrating": "Рабочее пространство обновляется. Пожалуйста, подождите..."
"WorkspaceIsMigrating": "Рабочее пространство обновляется. Пожалуйста, подождите...",
"FailedToLoadWorkspaces": "Не удалось загрузить список пространств",
"NoWorkspacesFound": "Пространства не найдены"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "Widget'ları yapılandır",
"Tab": "Sekme",
"WorkspaceIsArchived": "Çalışma alanı kullanılmadığı için arşivlendi. Geri yüklemek için lütfen bizimle iletişime geçin...",
"WorkspaceIsMigrating": "Çalışma alanı güncelleniyor. Lütfen bekleyin..."
"WorkspaceIsMigrating": "Çalışma alanı güncelleniyor. Lütfen bekleyin...",
"FailedToLoadWorkspaces": "Çalışma alanları yüklenemedi",
"NoWorkspacesFound": "Çalışma alanı bulunamadı"
}
}
4 changes: 3 additions & 1 deletion plugins/workbench-assets/lang/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"ConfigureWidgets": "配置小部件",
"Tab": "选项卡",
"WorkspaceIsArchived": "工作区因未使用而归档,请与我们联系以恢复...",
"WorkspaceIsMigrating": "工作区正在更新。请稍候..."
"WorkspaceIsMigrating": "工作区正在更新。请稍候...",
"FailedToLoadWorkspaces": "加载工作区失败",
"NoWorkspacesFound": "未找到工作区"
}
}
Loading
Loading