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}