diff --git a/scopes/docs/docs/overview/overview.tsx b/scopes/docs/docs/overview/overview.tsx
index 85325173a1d7..4fffcbe828b4 100644
--- a/scopes/docs/docs/overview/overview.tsx
+++ b/scopes/docs/docs/overview/overview.tsx
@@ -159,6 +159,7 @@ export function Overview({
viewport={null}
fullContentHeight
disableScroll={true}
+ propagateError={isMinimal}
sandbox={sandboxValue}
{...rest}
component={component}
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/card-overlays.module.scss b/scopes/workspace/workspace/ui/workspace/workspace-overview/card-overlays.module.scss
new file mode 100644
index 000000000000..3a2f399835f4
--- /dev/null
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/card-overlays.module.scss
@@ -0,0 +1,72 @@
+.changedPill {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 10px;
+ padding: 3px 8px;
+ border-radius: 999px;
+ background: var(--warning-surface-color);
+ color: var(--warning-color);
+ font-weight: 600;
+ letter-spacing: 0.02em;
+}
+
+.changedDot {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--warning-color);
+}
+
+.spinnerBadge {
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: var(--surface-color);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.spinnerArc {
+ transform-origin: center;
+ animation: spinSlow 0.9s linear infinite;
+}
+
+.buildingPreview {
+ position: relative;
+ height: 100%;
+}
+
+.dotsPattern {
+ position: absolute;
+ inset: 0;
+}
+
+.buildingPlaceholder {
+ position: absolute;
+ inset: 20% 14%;
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.55);
+ backdrop-filter: blur(2px);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 0 16px;
+ gap: 7px;
+}
+
+.queuedPreview {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+@keyframes spinSlow {
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/card-overlays.tsx b/scopes/workspace/workspace/ui/workspace/workspace-overview/card-overlays.tsx
new file mode 100644
index 000000000000..25e3c84549f6
--- /dev/null
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/card-overlays.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import styles from './card-overlays.module.scss';
+
+const accent = 'var(--bit-accent-color, #6c5ce7)';
+const accentAlpha = (pct: number) => `color-mix(in srgb, var(--bit-accent-color, #6c5ce7) ${pct}%, transparent)`;
+
+export function ChangedPill() {
+ return (
+
+
+ changed
+
+ );
+}
+
+export function BuildSpinner() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export function BuildingPreview() {
+ return (
+
+ );
+}
+
+export function QueuedPreview() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/filter-utils.ts b/scopes/workspace/workspace/ui/workspace/workspace-overview/filter-utils.ts
index 7508bee65061..5a5d3615a260 100644
--- a/scopes/workspace/workspace/ui/workspace/workspace-overview/filter-utils.ts
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/filter-utils.ts
@@ -1,17 +1,30 @@
-import type { WorkspaceItem } from './workspace-overview.types';
+import type { WorkspaceItem, ComponentStatus } from './workspace-overview.types';
export interface ActiveFilters {
namespaces: string[];
scopes: string[];
+ statuses: Set;
}
+export const ALL_STATUSES: ComponentStatus[] = ['built', 'changed', 'building', 'queued'];
+
export function parseActiveFilters(search: URLSearchParams): ActiveFilters {
return {
namespaces: (search.get('ns') || '').split(',').filter(Boolean),
scopes: (search.get('scopes') || '').split(',').filter(Boolean),
+ statuses: new Set(ALL_STATUSES),
};
}
+export function getComponentStatus(item: WorkspaceItem): ComponentStatus {
+ const buildStatus = (item.component as any).buildStatus;
+ const status = (item.component as any).status;
+ if (buildStatus === 'pending') return 'queued';
+ if (status?.modifyInfo?.hasModifiedFiles || status?.modifyInfo?.hasModifiedDependencies) return 'changed';
+ if (buildStatus === 'building') return 'building';
+ return 'built';
+}
+
export function filterItems(items: WorkspaceItem[], filters: ActiveFilters): WorkspaceItem[] {
return items.filter((item) => {
const ns = item.component.id.namespace || '/';
@@ -19,6 +32,10 @@ export function filterItems(items: WorkspaceItem[], filters: ActiveFilters): Wor
if (filters.namespaces.length && !filters.namespaces.includes(ns)) return false;
if (filters.scopes.length && !filters.scopes.includes(sc)) return false;
+ if (filters.statuses.size > 0 && filters.statuses.size < ALL_STATUSES.length) {
+ const componentStatus = getComponentStatus(item);
+ if (!filters.statuses.has(componentStatus)) return false;
+ }
return true;
});
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/hope-component-card.module.scss b/scopes/workspace/workspace/ui/workspace/workspace-overview/hope-component-card.module.scss
new file mode 100644
index 000000000000..cbfa73bd22ac
--- /dev/null
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/hope-component-card.module.scss
@@ -0,0 +1,162 @@
+.card {
+ position: relative;
+ border-radius: 10px;
+ overflow: hidden;
+ background: var(--surface-color);
+ border: 1px solid var(--border-medium-color);
+ cursor: pointer;
+ transition:
+ box-shadow 0.15s ease,
+ border-color 0.15s ease;
+
+ &:hover {
+ box-shadow: 0 4px 14px rgba(20, 0, 104, 0.06);
+ }
+}
+
+.cardBuilding {
+ composes: card;
+
+ &:hover {
+ box-shadow: none;
+ }
+}
+
+.linkWrapper {
+ text-decoration: none;
+ color: inherit;
+ display: block;
+}
+
+/* ---- Preview ---- */
+
+.preview {
+ height: 180px;
+ position: relative;
+ overflow: hidden;
+ background: white;
+ border-bottom: 1px solid var(--border-medium-color);
+}
+
+.previewQueued {
+ composes: preview;
+ background: var(--surface01-color);
+}
+
+.previewInner {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+}
+
+/* ---- Env badge ---- */
+
+.envBadge {
+ position: absolute;
+ bottom: 8px;
+ right: 8px;
+ background: var(--surface-color);
+ padding: 4px;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ z-index: 3;
+}
+
+.envIcon {
+ height: 14px;
+ display: block;
+}
+
+/* ---- Status corner ---- */
+
+.statusCorner {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 2;
+}
+
+/* ---- Footer ---- */
+
+.footer {
+ padding: 8px 12px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.name {
+ flex: 1;
+ min-width: 0;
+ font-size: 12.5px;
+ font-weight: 500;
+ font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace;
+ color: var(--on-background-color);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ letter-spacing: -0.01em;
+}
+
+.nameQueued {
+ composes: name;
+ color: var(--on-background-medium-color);
+}
+
+.hash {
+ font-size: 10.5px;
+ padding: 1px 6px;
+ border-radius: 4px;
+ background: var(--surface01-color);
+ font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace;
+ color: var(--on-background-low-color);
+ flex-shrink: 0;
+}
+
+.scopeBadge {
+ width: 20px;
+ height: 20px;
+ border-radius: 5px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ overflow: hidden;
+}
+
+.scopeBadgeIcon {
+ width: 12px;
+ height: 12px;
+ object-fit: contain;
+ filter: brightness(0) invert(1);
+}
+
+.scopeBadgeInitial {
+ font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace;
+ font-size: 10px;
+ font-weight: 700;
+ color: white;
+ line-height: 1;
+}
+
+.buildingLabel {
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ flex-shrink: 0;
+}
+
+/* ---- Load preview button ---- */
+
+.loadPreview {
+ position: absolute;
+ right: 4px !important;
+ left: unset !important;
+ z-index: 4;
+}
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/hope-component-card.tsx b/scopes/workspace/workspace/ui/workspace/workspace-overview/hope-component-card.tsx
new file mode 100644
index 000000000000..4aeac2ac1bb5
--- /dev/null
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/hope-component-card.tsx
@@ -0,0 +1,152 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Link } from '@teambit/base-react.navigation.link';
+import { PreviewPlaceholder } from '@teambit/preview.ui.preview-placeholder';
+import { Tooltip } from '@teambit/design.ui.tooltip';
+import { LoadPreview } from '@teambit/workspace.ui.load-preview';
+import { ComponentID } from '@teambit/component-id';
+import type { ComponentModel } from '@teambit/component';
+import type { ComponentDescriptor } from '@teambit/component-descriptor';
+import type { ScopeID } from '@teambit/scopes.scope-id';
+import { getComponentStatus } from './filter-utils';
+import { ChangedPill, BuildSpinner, BuildingPreview } from './card-overlays';
+import styles from './hope-component-card.module.scss';
+
+export type HopeComponentCardProps = {
+ component: ComponentModel;
+ componentDescriptor: ComponentDescriptor;
+ scope?: { id: ScopeID; icon?: string; backgroundIconColor?: string };
+ showPreview?: boolean;
+};
+
+export function HopeComponentCard({
+ component,
+ componentDescriptor,
+ scope,
+ showPreview: showPreviewProp,
+}: HopeComponentCardProps) {
+ const [shouldShowPreview, setShouldShowPreview] = useState(Boolean(showPreviewProp));
+ const prevServerUrlRef = useRef(component.server?.url);
+
+ useEffect(() => {
+ if (prevServerUrlRef.current !== component.server?.url && shouldShowPreview) {
+ setShouldShowPreview(false);
+ setTimeout(() => setShouldShowPreview(true), 50);
+ }
+ prevServerUrlRef.current = component.server?.url;
+ }, [component.server?.url]);
+
+ useEffect(() => {
+ setShouldShowPreview(Boolean(showPreviewProp));
+ }, [showPreviewProp]);
+
+ const item = { component } as any;
+ const status = getComponentStatus(item);
+ const accent = 'var(--bit-accent-color, #6c5ce7)';
+
+ const isBuilding = status === 'building';
+ const isQueued = status === 'queued';
+ const isChanged = status === 'changed';
+
+ const href = `${component.id.fullName}?scope=${component.id.scope}`;
+
+ const loadPreviewVisible = component.compositions.length > 0 && !isBuilding && !shouldShowPreview;
+
+ const showPreviewClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ setShouldShowPreview(true);
+ };
+
+ const envAspect = componentDescriptor.get('teambit.envs/envs');
+ const env = envAspect?.data || envAspect;
+ const envComponentId = env?.id ? ComponentID.fromString(env.id) : undefined;
+
+ const cardClass = isBuilding ? styles.cardBuilding : styles.card;
+ const buildingBorderStyle = isBuilding
+ ? {
+ borderColor: accent,
+ boxShadow: `0 0 0 3px color-mix(in srgb, var(--bit-accent-color, #6c5ce7) 10%, transparent)`,
+ }
+ : undefined;
+
+ const nameLabel = component.id.namespace ? `${component.id.namespace}/${component.id.name}` : component.id.name;
+
+ const shortHash = component.id.version?.slice(0, 7);
+
+ const scopeInitial = component.id.scope?.split('.').pop()?.charAt(0).toUpperCase();
+
+ return (
+
+ {loadPreviewVisible &&
}
+
+
+
+
+
+
+
+ {!isQueued && env?.icon && (
+
+
+
+
+
+ )}
+
+ {(isChanged || isBuilding) && (
+
+ {isChanged && }
+ {isBuilding && }
+
+ )}
+
+
+
+
+
+ {scope?.icon ? (
+
+ ) : (
+
{scopeInitial}
+ )}
+
+
+
{nameLabel}
+ {!isBuilding && !isQueued && shortHash &&
{shortHash} }
+ {isBuilding && (
+
+ BUILDING
+
+ )}
+
+
+
+ );
+}
+
+function CardPreview({
+ component,
+ componentDescriptor,
+ status,
+ shouldShowPreview,
+}: {
+ component: ComponentModel;
+ componentDescriptor: ComponentDescriptor;
+ status: string;
+ shouldShowPreview: boolean;
+}) {
+ if (status === 'building') return ;
+
+ return (
+
+ );
+}
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/namespace-header.module.scss b/scopes/workspace/workspace/ui/workspace/workspace-overview/namespace-header.module.scss
new file mode 100644
index 000000000000..da455d470942
--- /dev/null
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/namespace-header.module.scss
@@ -0,0 +1,88 @@
+.header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 3px;
+ flex-shrink: 0;
+}
+
+.name {
+ font-size: 20px;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ color: var(--on-background-color);
+ flex-shrink: 0;
+}
+
+.count {
+ font-size: 12px;
+ color: var(--on-background-low-color);
+ flex-shrink: 0;
+}
+
+.buildingPill {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 10.5px;
+ font-weight: 500;
+ padding: 1px 8px;
+ border-radius: 999px;
+ margin-left: 4px;
+ flex-shrink: 0;
+}
+
+.buildingDot {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ animation: pulse 1.4s ease-in-out infinite;
+}
+
+.scopeIconBadge {
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ overflow: hidden;
+}
+
+.scopeIconImg {
+ width: 14px;
+ height: 14px;
+ object-fit: contain;
+ filter: brightness(0) invert(1);
+}
+
+.scopeInitial {
+ font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace;
+ font-size: 11px;
+ font-weight: 700;
+ color: white;
+ line-height: 1;
+}
+
+.divider {
+ flex: 1;
+ height: 1px;
+ background: var(--border-medium-color);
+ margin-left: 4px;
+}
+
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.55;
+ }
+}
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/namespace-header.tsx b/scopes/workspace/workspace/ui/workspace/workspace-overview/namespace-header.tsx
new file mode 100644
index 000000000000..cd33f22e877a
--- /dev/null
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/namespace-header.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import { getComponentStatus } from './filter-utils';
+import type { WorkspaceItem } from './workspace-overview.types';
+import styles from './namespace-header.module.scss';
+
+export interface NamespaceHeaderProps {
+ namespace: string;
+ items: WorkspaceItem[];
+ scopeIcon?: string;
+ scopeIconColor?: string;
+}
+
+export function NamespaceHeader({ namespace, items, scopeIcon, scopeIconColor }: NamespaceHeaderProps) {
+ const accent = 'var(--bit-accent-color, #6c5ce7)';
+ const tint = 'color-mix(in srgb, var(--bit-accent-color, #6c5ce7) 12%, transparent)';
+
+ let buildingCount = 0;
+ let readyCount = 0;
+ for (const item of items) {
+ const s = getComponentStatus(item);
+ if (s === 'building') buildingCount++;
+ if (s === 'built' || s === 'changed') readyCount++;
+ }
+
+ return (
+
+
+ {namespace}
+
+ {readyCount}/{items.length}
+
+ {buildingCount > 0 && (
+
+
+ building
+
+ )}
+
+
+ );
+}
+
+function HeaderIcon({
+ scopeIcon,
+ scopeIconColor,
+ namespace,
+ accent,
+}: {
+ scopeIcon?: string;
+ scopeIconColor?: string;
+ namespace: string;
+ accent: string;
+}) {
+ if (scopeIcon) {
+ return (
+
+
+
+ );
+ }
+
+ if (scopeIconColor) {
+ const initial = namespace.split(/[./]/).pop()?.charAt(0).toUpperCase() || '?';
+ return (
+
+ {initial}
+
+ );
+ }
+
+ return ;
+}
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/use-workspace-aggregation.ts b/scopes/workspace/workspace/ui/workspace/workspace-overview/use-workspace-aggregation.ts
index b9398fd6e44d..5cd9eab9e747 100644
--- a/scopes/workspace/workspace/ui/workspace/workspace-overview/use-workspace-aggregation.ts
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/use-workspace-aggregation.ts
@@ -58,11 +58,17 @@ export function useWorkspaceAggregation(
const sortedScopes = [...map.keys()].sort();
- const groups: AggregationGroup[] = sortedScopes.map((sc) => ({
- name: sc,
- displayName: sc,
- items: sortItemsByNamespace(map.get(sc)!),
- }));
+ const groups: AggregationGroup[] = sortedScopes.map((sc) => {
+ const groupItems = map.get(sc)!;
+ const sampleScope = groupItems.find((i) => i.scope?.icon)?.scope;
+ return {
+ name: sc,
+ displayName: sc,
+ items: sortItemsByNamespace(groupItems),
+ scopeIcon: sampleScope?.icon,
+ scopeIconColor: sampleScope?.backgroundIconColor,
+ };
+ });
return {
groups,
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-filter-panel.tsx b/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-filter-panel.tsx
index b9ab3d1ca7aa..30dfa26e7281 100644
--- a/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-filter-panel.tsx
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-filter-panel.tsx
@@ -50,13 +50,15 @@ export function WorkspaceFilterPanel({
const activeNsOptions = activeNamespaces.map((v) => ({ value: v }));
const activeScopeOptions = activeScopes.map((v) => ({ value: v }));
- const applyNs = (opts) => {
- const list = opts.map((o) => o.value).filter((v): v is string => typeof v === 'string');
+ const applyNs = (opts: readonly { value?: string }[] | { value?: string } | null) => {
+ const arr = Array.isArray(opts) ? opts : opts ? [opts] : [];
+ const list = arr.map((o) => o.value).filter((v): v is string => typeof v === 'string');
onNamespacesChange(list);
};
- const applyScopes = (opts) => {
- const list = opts.map((o) => o.value).filter((v): v is string => typeof v === 'string');
+ const applyScopes = (opts: readonly { value?: string }[] | { value?: string } | null) => {
+ const arr = Array.isArray(opts) ? opts : opts ? [opts] : [];
+ const list = arr.map((o) => o.value).filter((v): v is string => typeof v === 'string');
onScopesChange(list);
};
@@ -71,8 +73,8 @@ export function WorkspaceFilterPanel({
};
return (
-
-
+
+
-
-
+
applyAgg(idx)}
options={availableAggregations.map((agg) => ({
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.module.scss b/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.module.scss
index bc3518a6c5ea..3cf92249f348 100644
--- a/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.module.scss
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.module.scss
@@ -1,211 +1,91 @@
@import '@teambit/ui-foundation.ui.constants.z-indexes/z-indexes.module.scss';
.container {
- padding: 24px 5% 150px 5%;
overflow-y: auto;
height: 100%;
box-sizing: border-box;
z-index: 1;
}
-.rightPreviewPlugins {
- display: flex;
- align-items: flex-end;
- justify-content: flex-end;
- width: 100%;
- height: 100%;
-}
-
-.envIcon {
- height: 14px;
+.content {
+ padding: 20px 40px 80px;
}
-.badge {
- background-color: var(--surface-color);
- padding: 4px;
- height: 15px;
- margin-right: 4px;
- z-index: $nav-z-index;
- border-radius: 8px;
- margin-bottom: 4px;
- box-shadow: 0px 2px 18px rgb(0 0 0 / 10%);
-}
-
-.cardGrid {
- display: grid;
- gap: 32px 24px;
- grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
- max-width: 1280px;
- grid-column-gap: 28px;
- grid-row-gap: 32px;
-}
+/* ---- Command bar ---- */
-.filterPanel {
+.commandBar {
display: flex;
align-items: center;
justify-content: space-between;
- margin-bottom: 24px;
- position: relative;
+ gap: 12px;
+ flex-wrap: wrap;
+ padding: 12px 40px;
+ background: color-mix(in srgb, var(--bit-accent-color, #6c5ce7) 3%, var(--background-color, #fff));
+ border-bottom: 1px solid var(--border-medium-color);
+ position: sticky;
+ top: 0;
z-index: $modal-z-index - 1;
}
-.aggButtons {
- display: inline-flex;
- background: var(--surface-1-color);
- border: 1px solid var(--border-medium-color);
- border-radius: 8px;
- padding: 2px;
- height: 34px;
-}
-
-.aggButton,
-.aggActive {
- padding: 4px 12px;
- font-size: 14px;
- border-radius: 6px;
- background: transparent;
- cursor: pointer;
- border: none;
+.leftCluster {
display: flex;
align-items: center;
-}
-
-.aggButton:hover {
- background: var(--surface-hover-color);
-}
-
-.aggActive {
- background: var(--brand-primary-color);
- color: white;
-}
-
-.dropdownList {
- max-height: 260px;
- overflow-y: auto;
- padding: 0 12px;
- display: flex;
- flex-direction: column;
gap: 8px;
+ flex-wrap: wrap;
}
-.dropdownItem {
+.rightCluster {
display: flex;
align-items: center;
gap: 8px;
- font-size: 14px;
- cursor: pointer;
-}
-
-.dropdownItem input {
- width: 16px;
- height: 16px;
-}
-
-.count {
- opacity: 0.55;
-}
-
-.agg {
- margin-bottom: 28px;
+ margin-left: auto;
}
-.aggregationTitle {
- margin-top: 0;
- margin-bottom: 15px;
+.aggToggle {
+ height: 32px !important;
+ font-size: 12.5px;
+ --surface02-color: color-mix(in srgb, var(--bit-accent-color, #6c5ce7) 20%, var(--surface-color, #f5f5f5));
}
-:global(.componentGrid) {
- grid-row-gap: 24px !important;
- grid-column-gap: 16px !important;
-}
+/* ---- Base filter overrides ---- */
-.filterPanel :global(.baseFilter) {
+.commandBar :global(.baseFilter) {
max-width: 220px;
height: 32px;
}
-.filterPanel :global(.control) {
+.commandBar :global(.control) {
border-radius: 8px !important;
height: 32px;
padding: 0 10px !important;
}
-.filterPanel :global(.menu) {
+.commandBar :global(.menu) {
z-index: $modal-z-index - 1 !important;
- border-radius: 8px !important;
+ border-radius: 14px !important;
padding: 8px 0 !important;
+ width: 260px;
+ box-shadow: 0 24px 60px -12px rgba(20, 0, 104, 0.2);
}
-.filterDropdown :global(.baseFilter) {
- height: 32px;
- max-width: 240px;
- font-size: 14px;
-}
-
-.filterDropdown :global(.baseFilter .control) {
- padding: 8px 12px;
- border-radius: 8px;
-}
-
-.filterDropdown :global(.menu) {
- margin-top: 10px;
- padding: 12px 0;
- min-width: 260px;
- border-radius: 12px;
-
- box-shadow: var(--bit-shadow-hover-low, 0 2px 8px rgba(0, 0, 0, 0.1));
- border: 1px solid var(--border-medium-color, #ededed);
-}
-
-.filterDropdown :global(.checkboxContainer) {
- padding-left: 14px;
-}
-
-.filterDropdown :global(.checkbox) {
- margin-right: 12px;
- accent-color: var(--bit-accent-color, #6c5ce7);
-}
-
-.filterDropdown :global(.buttonsSection) {
- padding: 12px 16px;
- border-top: 1px solid var(--border-medium-color, #ededed);
-}
-
-.filterDropdown :global(.placeholder > span:not(:first-child)) {
- margin-left: 8px;
-}
+/* ---- Section ---- */
-.filterPanel {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 24px;
- gap: 16px;
-}
-
-.leftFilters {
- display: flex;
- gap: 12px;
- align-items: center;
+.section {
+ margin-bottom: 40px;
}
-.rightAggToggle {
- display: flex;
- align-items: center;
- margin-left: auto;
- height: 32px;
- font-size: 14px;
+.sectionHeader {
+ position: sticky;
+ top: 57px;
+ z-index: $nav-z-index;
+ background: var(--background-color);
+ padding: 12px 0 18px;
}
-.toggleBtn {
- height: 32px !important;
- font-size: 14px;
-}
+/* ---- Card grid ---- */
.cardGrid {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
- grid-row-gap: 32px;
- grid-column-gap: 24px;
- max-width: 1280px;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 14px;
}
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.tsx b/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.tsx
index 8089b5c74e4d..f60a151c0a99 100644
--- a/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.tsx
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.tsx
@@ -1,21 +1,15 @@
import React, { useContext, useMemo } from 'react';
import { ComponentGrid } from '@teambit/explorer.ui.gallery.component-grid';
import { EmptyWorkspace } from '@teambit/workspace.ui.empty-workspace';
-import { PreviewPlaceholder } from '@teambit/preview.ui.preview-placeholder';
-import { Tooltip } from '@teambit/design.ui.tooltip';
-import { ComponentID } from '@teambit/component-id';
-import type { ComponentModel } from '@teambit/component';
import compact from 'lodash.compact';
import { ScopeID } from '@teambit/scopes.scope-id';
import { useCloudScopes } from '@teambit/cloud.hooks.use-cloud-scopes';
-import { WorkspaceComponentCard } from '@teambit/workspace.ui.workspace-component-card';
-import type { ComponentCardPluginType, PluginProps } from '@teambit/explorer.ui.component-card';
import { useWorkspaceMode } from '@teambit/workspace.ui.use-workspace-mode';
-import { H3 } from '@teambit/design.ui.heading';
import { WorkspaceContext } from '../workspace-context';
-import { LinkPlugin } from './link-plugin';
import { useWorkspaceAggregation } from './use-workspace-aggregation';
import { useQueryParamWithDefault, useListParamWithDefault } from './use-query-param-with-default';
+import { NamespaceHeader } from './namespace-header';
+import { HopeComponentCard } from './hope-component-card';
import type { AggregationType } from './workspace-overview.types';
import { WorkspaceFilterPanel } from './workspace-filter-panel';
import styles from './workspace-overview.module.scss';
@@ -27,8 +21,6 @@ export function WorkspaceOverview() {
if (!components.length) return ;
const { isMinimal } = useWorkspaceMode();
- const compModelsById = useMemo(() => new Map(components.map((c) => [c.id.toString(), c])), [components]);
-
const uniqueScopes = [...new Set(components.map((c) => c.id.scope))];
const { cloudScopes } = useCloudScopes(uniqueScopes);
const cloudMap = new Map((cloudScopes || []).map((s) => [s.id.toString(), s]));
@@ -48,7 +40,13 @@ export function WorkspaceOverview() {
(ScopeID.isValid(component.id.scope) && { id: ScopeID.fromString(component.id.scope) }) ||
undefined;
- return { component, componentDescriptor: descriptor, scope: (scope && { id: scope.id }) || undefined };
+ return {
+ component,
+ componentDescriptor: descriptor,
+ scope: scope
+ ? { id: scope.id, icon: (scope as any).icon, backgroundIconColor: (scope as any).backgroundIconColor }
+ : undefined,
+ };
})
);
@@ -57,7 +55,7 @@ export function WorkspaceOverview() {
const [activeScopes, setActiveScopes] = useListParamWithDefault('scopes');
const filters = useMemo(
- () => ({ namespaces: activeNamespaces, scopes: activeScopes }),
+ () => ({ namespaces: activeNamespaces, scopes: activeScopes, statuses: new Set() as any }),
[activeNamespaces, activeScopes]
);
@@ -67,8 +65,6 @@ export function WorkspaceOverview() {
filters
);
- const plugins = useCardPlugins({ compModelsById, showPreview: isMinimal });
-
return (
- {filteredCount === 0 && }
-
- {groups.map((group) => (
-
- {groupType !== 'none' && {group.displayName} }
-
-
- {group.items.map((item) => (
-
- ))}
-
-
- ))}
-
- );
-}
-
-export function useCardPlugins({
- compModelsById,
- showPreview,
-}: {
- compModelsById: Map;
- showPreview?: boolean;
-}): ComponentCardPluginType[] {
- const serverUrlsSignature = React.useMemo(() => {
- const serversCount = Array.from(compModelsById.values())
- .filter((comp) => comp.server?.url)
- .map((comp) => comp.server?.url)
- .join(',');
- return serversCount;
- }, [compModelsById]);
-
- const plugins = React.useMemo(
- () => [
- {
- preview: function Preview({ component, shouldShowPreview }) {
- const compModel = compModelsById.get(component.id.toString());
- if (!compModel) return null;
- return (
-
- );
- },
- },
- {
- previewBottomRight: function PreviewBottomRight({ component }) {
- const env = component.get('teambit.envs/envs');
- const envComponentId = env?.id ? ComponentID.fromString(env?.id) : undefined;
-
- return (
-
-
-
-
-
+
+ {filteredCount === 0 &&
}
+
+ {groups.map((group) => (
+
+ {groupType !== 'none' && (
+
+
-
- );
- },
- },
- new LinkPlugin(),
- ],
- [compModelsById.size, serverUrlsSignature, showPreview]
+ )}
+
+
+ {group.items.map((item) => (
+
+ ))}
+
+
+ ))}
+
+
);
-
- return plugins;
}
diff --git a/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.types.ts b/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.types.ts
index 1630930a4978..a101ce8f6d6f 100644
--- a/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.types.ts
+++ b/scopes/workspace/workspace/ui/workspace/workspace-overview/workspace-overview.types.ts
@@ -5,15 +5,21 @@ import type { ScopeID } from '@teambit/scopes.scope-id';
export interface WorkspaceItem {
component: ComponentModel;
componentDescriptor: ComponentDescriptor;
- scope?: { id: ScopeID };
+ scope?: { id: ScopeID; icon?: string; backgroundIconColor?: string };
}
export type AggregationType = 'namespaces' | 'scopes' | 'none';
+export type Density = 'compact' | 'comfy';
+
+export type ComponentStatus = 'built' | 'changed' | 'building' | 'queued';
+
export interface AggregationGroup {
name: string;
displayName: string;
items: WorkspaceItem[];
+ scopeIcon?: string;
+ scopeIconColor?: string;
}
export interface AggregationResult {
diff --git a/scopes/workspace/workspace/ui/workspace/workspace.module.scss b/scopes/workspace/workspace/ui/workspace/workspace.module.scss
index 9d225d21cd03..c32b0b00404e 100644
--- a/scopes/workspace/workspace/ui/workspace/workspace.module.scss
+++ b/scopes/workspace/workspace/ui/workspace/workspace.module.scss
@@ -18,37 +18,20 @@
}
}
-.minimalCorner {
- width: fit-content;
- height: 46px !important;
- background: var(--background-color, #ededed) !important;
-
- &.dark {
- --bit-border-color-lightest: #333333;
- --bit-text-color-heavy: white;
-
- > a > img {
- filter: invert(1);
- }
- }
-
- &.light {
- --background-color: #ededed;
- }
-
- > a {
- &[aria-current='page'] > img {
- filter: invert(31%) sepia(75%) saturate(3183%) hue-rotate(235deg) brightness(99%) contrast(108%);
- }
-
- > img {
- width: 22px;
- height: 22px;
+.backButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ color: var(--on-background-color, var(--bit-text-color-heavy, #2b2b2b));
+ text-decoration: none;
+ transition: background 0.15s ease;
+ margin-left: 8px;
- &:hover {
- filter: invert(31%) sepia(75%) saturate(3183%) hue-rotate(235deg) brightness(99%) contrast(108%);
- }
- }
+ &:hover {
+ background: color-mix(in srgb, var(--on-background-color, #2b2b2b) 8%, transparent);
}
}
@@ -96,6 +79,28 @@
flex-direction: column;
overflow: hidden; // TODO
height: 100vh;
+
+ @media (hover: hover) {
+ * {
+ &::-webkit-scrollbar {
+ width: 14px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: var(--background-color, #fff);
+ }
+
+ &::-webkit-scrollbar-thumb {
+ border: 5px solid var(--background-color, #fff);
+ background: var(--border-medium-color, #d0d0d0);
+ border-radius: 100vmax;
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background: var(--on-background-color, var(--bit-text-color-light, #999));
+ }
+ }
+ }
}
.topbar {
@@ -135,6 +140,9 @@
&.minimal {
height: 46px;
+ --bit-bg-heaviest: color-mix(in srgb, var(--bit-accent-color, #6c5ce7) 3%, var(--background-color, #fff));
+ --bit-border-color-lightest: var(--bit-bg-heaviest);
+ border-bottom: 1px solid var(--border-medium-color, #e6e6e6);
}
}
diff --git a/scopes/workspace/workspace/ui/workspace/workspace.tsx b/scopes/workspace/workspace/ui/workspace/workspace.tsx
index 6f576903edcd..0b461e8454dd 100644
--- a/scopes/workspace/workspace/ui/workspace/workspace.tsx
+++ b/scopes/workspace/workspace/ui/workspace/workspace.tsx
@@ -86,6 +86,9 @@ export function Workspace({ routeSlot, menuSlot, sidebar, workspaceUI, onSidebar
workspaceUI.setComponents(workspace.components);
const inIframe = typeof window !== 'undefined' && window.parent && window.parent !== window;
+ const location = useLocation();
+ const isOverview = location.pathname === '/' || location.pathname === '';
+ const showTopBar = !isMinimal || (isMinimal && !isOverview);
return (
@@ -94,23 +97,37 @@ export function Workspace({ routeSlot, menuSlot, sidebar, workspaceUI, onSidebar
{isMinimal && inIframe && }
- {
+ {showTopBar && (
(
-
+ {isMinimal ? (
+
+
+
+
+
+ ) : (
+
+ )}
{isMinimal &&
}
)}
// @ts-ignore - getting an error of "Types have separate declarations of a private property 'registerFn'." for some reason after upgrading teambit.harmony/harmony from 0.4.6 to 0.4.7
menu={menuSlot}
/>
- }
+ )}
{sidebar}