refactor: unify guided workspace empty states
This commit is contained in:
@@ -205,7 +205,7 @@ test('uses one focused Semi bottom SideSheet for the complete mobile history que
|
||||
mocks.historyExports.mockResolvedValue([]);
|
||||
renderPage('/history');
|
||||
|
||||
await screen.findByText('选择车辆开始查询');
|
||||
await screen.findByText('选择车辆,开始核验历史证据');
|
||||
const trigger = screen.getByRole('button', { name: '修改查询范围:请选择车辆' });
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.queryByRole('button', { name: '展开趋势' })).not.toBeInTheDocument();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';
|
||||
import { IconChevronRight, IconClose, IconDownload, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
||||
import { IconCalendar, IconChevronRight, IconClose, IconDownload, IconMapPin, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
||||
import { Button, Card, CardGroup, Checkbox, Descriptions, Dropdown, Empty, Input, Progress, Select, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { FormEvent, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
@@ -8,7 +8,7 @@ import type { HistoryDataResponse, HistoryDataRow, HistoryExportJob, HistoryExpo
|
||||
import { buildHistorySeriesPanels, formatExportFileSize, formatHistoryValue, formatSeriesGrain, historyExportPollInterval, parseHistoryKeywords } from '../domain/history';
|
||||
import { downloadBlob } from '../domain/download';
|
||||
import { formatShanghaiDateTime } from '../domain/formatters';
|
||||
import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState';
|
||||
import { InlineError, PanelEmpty, PanelLoading, WorkspaceEmptyGuide } from '../shared/AsyncState';
|
||||
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
|
||||
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
|
||||
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
|
||||
@@ -593,11 +593,26 @@ export default function HistoryPage() {
|
||||
{dataQuery.isPending && keywords.length
|
||||
? <PanelLoading className="v2-history-loading" title="正在加载当前筛选范围的历史数据" description="按车辆、时间和协议整理可追溯证据。" />
|
||||
: !resultRows.length
|
||||
? <PanelEmpty className="v2-history-empty"
|
||||
title={keywords.length ? '当前条件没有历史记录' : '选择车辆开始查询'}
|
||||
description={keywords.length ? '调整车辆、时间或数据来源后重试。' : '支持车牌或 VIN,最多可同时查询 5 台车辆。'}
|
||||
action={mobileLayout ? <Button className="v2-history-empty-action" theme="solid" icon={<IconSearch />} aria-label={keywords.length ? '调整查询范围' : '选择车辆并查询'} onClick={() => setFiltersCollapsed(false)}>{keywords.length ? '调整查询范围' : '选择车辆并查询'}</Button> : null}
|
||||
/>
|
||||
? keywords.length
|
||||
? <PanelEmpty
|
||||
className="v2-history-empty"
|
||||
title="当前条件没有历史记录"
|
||||
description="调整车辆、时间或数据来源后重试。"
|
||||
action={<Button className="v2-history-empty-action" theme="light" icon={<IconSearch />} aria-label="调整查询范围" onClick={() => setFiltersCollapsed(false)}>调整查询范围</Button>}
|
||||
/>
|
||||
: <WorkspaceEmptyGuide
|
||||
className="v2-history-empty"
|
||||
eyebrow="从一个明确的车辆范围开始"
|
||||
icon={<IconSearch />}
|
||||
title="选择车辆,开始核验历史证据"
|
||||
description="一次最多查询 5 台车辆,结果会保留协议来源、质量说明与 RAW 证据。"
|
||||
steps={[
|
||||
{ icon: <IconSearch />, title: '选择车辆', description: '支持车牌、VIN 与批量粘贴' },
|
||||
{ icon: <IconCalendar />, title: '确认时间', description: '按上海时区精确到分钟' },
|
||||
{ icon: <IconMapPin />, title: '核验数据', description: '位置、RAW 与日里程统一检索' }
|
||||
]}
|
||||
action={<Button className="v2-history-empty-action" theme="solid" icon={<IconSearch />} aria-label="选择车辆并查询" onClick={() => setFiltersCollapsed(false)}>选择车辆并查询</Button>}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
{keywords.length ? <footer><TablePagination page={page} totalPages={totalPages} info={`共 ${(result?.total ?? 0).toLocaleString('zh-CN')} 条`} onPageChange={(next) => setOffset((next - 1) * limit)} pageSize={mobileLayout ? undefined : limit} onPageSizeChange={(next) => { setLimit(next); setOffset(0); }} pageSizeOptions={mobileLayout ? undefined : [{ value: 20, label: '20 条/页' }, { value: 50, label: '50 条/页' }, { value: 100, label: '100 条/页' }]} /></footer> : null}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { TrackPlaybackEvent, TrackPlaybackResponse, VehicleRow } from '../.
|
||||
import { protocolDisplayLabel } from '../domain/protocols';
|
||||
import { buildTrackRailSelection, downloadTrackCsv, formatDuration, sampledEventIndex, TRACK_ADDRESS_SETTLE_MS, trackAddressCoordinate, trackPlaybackInterval, trackPointMileageAvailable, type TrackAddressCoordinate } from '../domain/track';
|
||||
import { TrackMap } from '../map/TrackMap';
|
||||
import { InlineError } from '../shared/AsyncState';
|
||||
import { InlineError, WorkspaceEmptyGuide } from '../shared/AsyncState';
|
||||
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
|
||||
import { ProtocolTag } from '../shared/ProtocolTag';
|
||||
import { SegmentedTabs } from '../shared/SegmentedTabs';
|
||||
@@ -513,7 +513,18 @@ export default function TrackPage() {
|
||||
</div>
|
||||
<p title={addressSettling ? undefined : addressQuery.data?.formattedAddress}>{playing ? '播放中,暂停地址解析' : addressSettling ? '位置已变化,等待地址解析…' : addressQuery.isFetching ? '地址解析中…' : addressQuery.data?.formattedAddress || `${current?.longitude.toFixed(6)}, ${current?.latitude.toFixed(6)}`}</p>
|
||||
</Card>
|
||||
</> : <Card className="v2-track-empty-state" bodyStyle={{ padding: 0 }}><Empty image={<IconMapPin />} title="先选择车辆,再开始轨迹回放" description="地图会显示完整路径、停留点、事件点和播放位置。"><Button theme="solid" icon={<IconSearch />} onClick={() => setRailCollapsed(false)}>选择车辆</Button></Empty></Card>}
|
||||
</> : <Card className="v2-track-empty-state" bodyStyle={{ padding: 0 }}><WorkspaceEmptyGuide
|
||||
eyebrow="轨迹回放"
|
||||
icon={<IconMapPin />}
|
||||
title="先选择车辆,再开始轨迹回放"
|
||||
description="地图会呈现完整路径、停留点、事件点与逐帧播放位置。"
|
||||
steps={[
|
||||
{ icon: <IconSearch />, title: '选择车辆', description: '车牌或 VIN 精确定位' },
|
||||
{ icon: <IconMapPin />, title: '确认来源', description: '自动选择最佳定位来源' },
|
||||
{ icon: <IconPlay />, title: '播放核验', description: '逐点查看速度、里程与事件' }
|
||||
]}
|
||||
action={<Button theme="solid" icon={<IconSearch />} onClick={() => setRailCollapsed(false)}>选择车辆</Button>}
|
||||
/></Card>}
|
||||
|
||||
{query.isFetching ? <div className="v2-track-loading"><span className="v2-spinner" />正在读取并加工历史轨迹</div> : null}
|
||||
{query.isError ? <div className="v2-track-error"><InlineError message={query.error instanceof Error ? query.error.message : '轨迹查询失败'} onRetry={() => query.refetch()} /></div> : null}
|
||||
|
||||
@@ -183,7 +183,8 @@ describe('V2 production entry', () => {
|
||||
expect(corePageSources.HistoryPage).toContain('<Descriptions className="v2-evidence-values"');
|
||||
expect(corePageSources.HistoryPage).toContain('<Card className={`v2-export-job-card');
|
||||
expect(corePageSources.HistoryPage).toContain('<Progress className="v2-export-job-progress"');
|
||||
expect(corePageSources.HistoryPage).toContain('<PanelEmpty className="v2-history-empty"');
|
||||
expect(corePageSources.HistoryPage).toContain('<PanelEmpty');
|
||||
expect(corePageSources.HistoryPage).toContain('<WorkspaceEmptyGuide');
|
||||
expect(corePageSources.HistoryPage).toContain('<PanelLoading className="v2-history-loading"');
|
||||
expect(corePageSources.HistoryPage).not.toContain('row ? <><dl>');
|
||||
expect(corePageSources.HistoryPage).not.toContain('job) => <article');
|
||||
@@ -412,7 +413,8 @@ describe('V2 production entry', () => {
|
||||
expect(corePageSources.TrackPage).toContain('<List className="v2-track-evidence-list v2-track-stop-list"');
|
||||
expect(corePageSources.TrackPage).toContain('<List className="v2-track-evidence-list v2-track-event-list"');
|
||||
expect(corePageSources.TrackPage).toContain('<List.Item className="v2-track-evidence-item"');
|
||||
expect(corePageSources.TrackPage).toContain('<Empty image={<IconMapPin />} title="先选择车辆,再开始轨迹回放"');
|
||||
expect(corePageSources.TrackPage).toContain('<WorkspaceEmptyGuide');
|
||||
expect(corePageSources.TrackPage).toContain('title="先选择车辆,再开始轨迹回放"');
|
||||
expect(corePageSources.TrackPage).toContain("from '../shared/WorkspaceSideSheet'");
|
||||
expect(corePageSources.TrackPage).toContain('<WorkspaceSideSheet');
|
||||
expect(corePageSources.TrackPage).toContain('variant="editor"');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
import { EmptyState, InlineError, PageLoading, PanelEmpty, PanelLoading } from './AsyncState';
|
||||
import { EmptyState, InlineError, PageLoading, PanelEmpty, PanelLoading, WorkspaceEmptyGuide } from './AsyncState';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -29,6 +29,27 @@ test('keeps empty states consistent while allowing a clear next action', () => {
|
||||
expect(action).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('renders a guided Semi empty workspace with clear ordered actions', () => {
|
||||
const action = vi.fn();
|
||||
const view = render(<WorkspaceEmptyGuide
|
||||
eyebrow="历史证据"
|
||||
title="选择车辆开始查询"
|
||||
description="按范围读取可追溯数据。"
|
||||
steps={[
|
||||
{ title: '选择车辆', description: '车牌或 VIN' },
|
||||
{ title: '确认时间', description: '上海时区' },
|
||||
{ title: '核验数据', description: '位置与里程' }
|
||||
]}
|
||||
action={<button onClick={action}>开始查询</button>}
|
||||
/>);
|
||||
|
||||
expect(view.container.querySelector('.v2-workspace-empty-guide')).toBeInTheDocument();
|
||||
expect(view.container.querySelectorAll('.v2-workspace-empty-guide-step.semi-card')).toHaveLength(3);
|
||||
expect(screen.getByText('历史证据')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '开始查询' }));
|
||||
expect(action).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('announces recoverable errors and exposes a Semi retry action', () => {
|
||||
const retry = vi.fn();
|
||||
render(<InlineError message="服务暂时不可用" onRetry={retry} />);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconAlertTriangle, IconRefresh } from '@douyinfe/semi-icons';
|
||||
import { Button, Empty, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
import { Button, Card, CardGroup, Empty, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type StateScope = 'page' | 'panel' | 'inline';
|
||||
@@ -75,6 +75,51 @@ export function PanelEmpty({
|
||||
return <StateSurface className={className} kind="empty" scope="panel" title={title} description={description} action={action} />;
|
||||
}
|
||||
|
||||
export type WorkspaceEmptyGuideStep = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: ReactNode;
|
||||
};
|
||||
|
||||
export function WorkspaceEmptyGuide({
|
||||
title,
|
||||
description,
|
||||
steps,
|
||||
icon,
|
||||
eyebrow,
|
||||
action,
|
||||
secondaryAction,
|
||||
className = ''
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
steps: WorkspaceEmptyGuideStep[];
|
||||
icon?: ReactNode;
|
||||
eyebrow?: string;
|
||||
action?: ReactNode;
|
||||
secondaryAction?: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const classes = ['v2-state-surface', 'is-empty', 'is-panel', 'v2-workspace-empty-guide', className].filter(Boolean).join(' ');
|
||||
|
||||
return <section className={classes} role="status" aria-live="polite">
|
||||
{eyebrow ? <Typography.Text className="v2-workspace-empty-guide-eyebrow" type="tertiary">{eyebrow}</Typography.Text> : null}
|
||||
<Empty
|
||||
className="v2-workspace-empty-guide-message"
|
||||
image={icon ? <span className="v2-workspace-empty-guide-icon">{icon}</span> : undefined}
|
||||
title={title}
|
||||
description={description}
|
||||
/>
|
||||
<CardGroup className="v2-workspace-empty-guide-steps" type="grid" spacing={0}>
|
||||
{steps.map((step, index) => <Card key={`${step.title}-${index}`} className="v2-workspace-empty-guide-step" bodyStyle={{ padding: 0 }}>
|
||||
<span className="v2-workspace-empty-guide-step-index">{step.icon ?? index + 1}</span>
|
||||
<span><Typography.Text strong>{step.title}</Typography.Text><Typography.Text type="tertiary" size="small">{step.description}</Typography.Text></span>
|
||||
</Card>)}
|
||||
</CardGroup>
|
||||
{action || secondaryAction ? <div className="v2-workspace-empty-guide-actions">{action}{secondaryAction}</div> : null}
|
||||
</section>;
|
||||
}
|
||||
|
||||
export function InlineError({ message, onRetry }: { message: string; onRetry?: () => void }) {
|
||||
return <StateSurface
|
||||
className="v2-inline-state"
|
||||
|
||||
@@ -23363,3 +23363,262 @@
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Guided Semi UI empty workspaces.
|
||||
* Empty result canvases should explain the task path instead of leaving a
|
||||
* large, visually inert panel. History and track playback share this contract.
|
||||
*/
|
||||
.v2-workspace-empty-guide {
|
||||
display: flex;
|
||||
width: min(760px, calc(100% - 40px));
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
padding: 26px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-eyebrow.semi-typography {
|
||||
margin-bottom: 9px;
|
||||
color: #6f7f92;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-message.semi-empty {
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-message .semi-empty-image {
|
||||
display: grid;
|
||||
width: auto;
|
||||
height: auto;
|
||||
place-items: center;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-icon {
|
||||
display: grid;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
place-items: center;
|
||||
border: 1px solid #cfe0fa;
|
||||
border-radius: 14px;
|
||||
background: #edf5ff;
|
||||
color: var(--v2-blue);
|
||||
box-shadow: 0 8px 20px rgba(18, 104, 223, .1);
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-icon .semi-icon {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-message .semi-empty-title {
|
||||
margin-top: 0;
|
||||
color: #25384f;
|
||||
font-size: 18px;
|
||||
font-weight: 750;
|
||||
letter-spacing: -.01em;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-message .semi-empty-description {
|
||||
max-width: 560px;
|
||||
margin-top: 7px;
|
||||
color: #748398;
|
||||
font-size: 12px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-steps.semi-card-group {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 21px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step.semi-card {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e0e7f0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
box-shadow: 0 3px 12px rgba(31, 48, 70, .04);
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step > .semi-card-body {
|
||||
display: grid;
|
||||
min-height: 72px;
|
||||
grid-template-columns: 32px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px !important;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step-index {
|
||||
display: grid;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
place-items: center;
|
||||
border-radius: 9px;
|
||||
background: #f0f5fc;
|
||||
color: #3976cc;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step-index .semi-icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step > .semi-card-body > span:last-child {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step .semi-typography {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step .semi-typography:first-child {
|
||||
color: #33485f;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step .semi-typography:last-child {
|
||||
color: #8290a2;
|
||||
font-size: 9px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-actions > .semi-button {
|
||||
min-height: 38px;
|
||||
border-radius: 8px;
|
||||
padding-inline: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.v2-history-empty.v2-workspace-empty-guide {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.v2-track-empty-state.semi-card {
|
||||
width: min(680px, calc(100% - 44px));
|
||||
}
|
||||
|
||||
.v2-track-empty-state > .semi-card-body > .v2-workspace-empty-guide {
|
||||
width: 100%;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.v2-track-empty-state .v2-workspace-empty-guide-message .semi-empty-description {
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.v2-track-empty-state .v2-workspace-empty-guide strong {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.v2-workspace-empty-guide {
|
||||
width: calc(100% - 20px);
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-eyebrow.semi-typography {
|
||||
margin-bottom: 7px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-message .semi-empty-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-message .semi-empty-description {
|
||||
max-width: 310px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-steps.semi-card-group {
|
||||
gap: 6px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step > .semi-card-body {
|
||||
min-height: 92px;
|
||||
grid-template-columns: 1fr;
|
||||
align-content: start;
|
||||
justify-items: center;
|
||||
gap: 7px;
|
||||
padding: 9px 6px !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step-index {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step .semi-typography:first-child {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-step .semi-typography:last-child {
|
||||
display: -webkit-box;
|
||||
font-size: 8px;
|
||||
line-height: 1.35;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-actions {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.v2-workspace-empty-guide-actions > .semi-button {
|
||||
min-width: 168px;
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.v2-history-empty.v2-workspace-empty-guide {
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.v2-track-empty-state.semi-card {
|
||||
width: calc(100% - 24px);
|
||||
transform: translate(-50%, calc(-50% - 44px));
|
||||
}
|
||||
|
||||
.v2-track-empty-state > .semi-card-body > .v2-workspace-empty-guide {
|
||||
padding: 17px 9px;
|
||||
}
|
||||
|
||||
.v2-track-empty-state .v2-workspace-empty-guide-message .semi-empty-description {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user