Merge pull request 'feat(sf-fe-a4): analysis sidebar (scrollspy) + breadcrumb + user avatar' (#344) from feat/sf-fe-a4-sidebar into main
This commit is contained in:
commit
956e461b9f
5 changed files with 487 additions and 105 deletions
|
|
@ -17,6 +17,7 @@
|
|||
"echarts-for-react": "^3.0.6",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "^15.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
|
|
|||
|
|
@ -1,72 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import Link from "next/link";
|
||||
import { LayoutDashboard } from "lucide-react";
|
||||
|
||||
// ── Placeholder components ─────────────────────────────────────────────────
|
||||
// Replaced in Wave 2–4 with real sections
|
||||
import { AnalysisSidebar } from "@/components/site-finder/analysis/AnalysisSidebar";
|
||||
import { AnalysisBreadcrumb } from "@/components/site-finder/analysis/AnalysisBreadcrumb";
|
||||
import { UserAvatar } from "@/components/site-finder/analysis/UserAvatar";
|
||||
|
||||
function SidebarPlaceholder() {
|
||||
return (
|
||||
<aside
|
||||
style={{
|
||||
width: 240,
|
||||
flexShrink: 0,
|
||||
background: "var(--bg-card)",
|
||||
borderRight: "1px solid var(--border-card)",
|
||||
padding: "16px 12px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
minHeight: "calc(100vh - 56px)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: "0.04em",
|
||||
color: "var(--fg-tertiary)",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
TODO A4: AnalysisSidebar
|
||||
</p>
|
||||
{/* Numbered nav placeholders */}
|
||||
{[
|
||||
"1. Инфо об участке",
|
||||
"2. Сети и точки подключения",
|
||||
"3. Конкуренты",
|
||||
"4. Оценка участка",
|
||||
"5. Атмосфера / воздух",
|
||||
].map((label) => (
|
||||
<div
|
||||
key={label}
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
borderRadius: 8,
|
||||
background: "var(--bg-card-alt)",
|
||||
color: "var(--fg-secondary)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "auto",
|
||||
fontSize: 12,
|
||||
color: "var(--fg-tertiary)",
|
||||
}}
|
||||
>
|
||||
TODO A4: «Источники» внизу sidebar
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
// ── Section placeholder (Wave 3–4 refactor will replace these) ─────────────────
|
||||
|
||||
interface SectionPlaceholderProps {
|
||||
id: string;
|
||||
|
|
@ -145,7 +86,7 @@ export default function AnalysisPage({ params }: PageProps) {
|
|||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Header / breadcrumb */}
|
||||
{/* ── Header ─────────────────────────────────────────────────────────── */}
|
||||
<header
|
||||
style={{
|
||||
background: "var(--bg-card)",
|
||||
|
|
@ -154,42 +95,25 @@ export default function AnalysisPage({ params }: PageProps) {
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
flexWrap: "wrap" as const,
|
||||
flexWrap: "wrap",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
minHeight: 56,
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
{/* TODO A4: replace with <AnalysisBreadcrumb cad={cad}/> */}
|
||||
<Link
|
||||
href="/site-finder"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: "var(--fg-secondary)",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
← К карте
|
||||
</Link>
|
||||
<span style={{ color: "var(--border-soft)" }}>·</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "var(--fg-primary)",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
{cad}
|
||||
</span>
|
||||
<span style={{ color: "var(--border-soft)" }}>·</span>
|
||||
<span style={{ fontSize: 13, color: "var(--fg-secondary)" }}>
|
||||
TODO A4: район · площадь · анализ свежий N мин
|
||||
</span>
|
||||
{/* TODO A4: <UserAvatar/> справа */}
|
||||
<AnalysisBreadcrumb cadNum={cad} />
|
||||
|
||||
{/* Push avatar to the right */}
|
||||
<div style={{ marginLeft: "auto" }}>
|
||||
<UserAvatar />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Body: sidebar + scrollable sections */}
|
||||
{/* ── Body: sidebar + scrollable sections ───────────────────────────── */}
|
||||
<div style={{ display: "flex", flex: 1 }}>
|
||||
{/* TODO A4: replace with <AnalysisSidebar/> */}
|
||||
<SidebarPlaceholder />
|
||||
<AnalysisSidebar />
|
||||
|
||||
{/* Main scrollable content */}
|
||||
<article
|
||||
|
|
@ -200,38 +124,55 @@ export default function AnalysisPage({ params }: PageProps) {
|
|||
maxWidth: 1000,
|
||||
}}
|
||||
>
|
||||
{/* TODO A5: Section 1 — HeadlineBar + 4 KPI + mini-map + EgrnPropertyTable + PoiList2Gis */}
|
||||
{/* Section 1 — TODO A5 */}
|
||||
<SectionPlaceholder
|
||||
id="section-1"
|
||||
title="1. Инфо об участке"
|
||||
title="1. Объект"
|
||||
note="TODO A5: HeadlineBar вердикт + 4 KPI (Площадь / Район / Медиана / Score) + mini-map + EGRN table + POI list 2GIS"
|
||||
/>
|
||||
|
||||
{/* TODO A6: Section 2 — NspdEngineeringNearbyBlock */}
|
||||
{/* Section 2 — TODO A6 */}
|
||||
<SectionPlaceholder
|
||||
id="section-2"
|
||||
title="2. Сети и точки подключения"
|
||||
title="2. Земля и риски"
|
||||
note="TODO A6: reuse NspdEngineeringNearbyBlock в новом layout headline+map"
|
||||
/>
|
||||
|
||||
{/* TODO A7/A8/A9: Section 3 — sub-sections 3.1 / 3.2 / 3.3 + CompetitorTable */}
|
||||
{/* Section 3 — TODO A7/A8/A9 */}
|
||||
<SectionPlaceholder
|
||||
id="section-3"
|
||||
title="3. Конкуренты"
|
||||
title="3. Рынок"
|
||||
note="TODO A7: WeightProfilePanel (настройки выборки) · A8: BestLayoutsBlock / Pipeline24moBlock / VelocityBlock · A9: CompetitorTable с drawer drill-in"
|
||||
/>
|
||||
|
||||
{/* TODO A10: Section 4 — ScoreBreakdownPanel + GateVerdictBanner + geology/hydro/risk */}
|
||||
{/* Sub-section anchor targets (invisible) for scrollspy / direct links */}
|
||||
<div
|
||||
id="section-3-1"
|
||||
style={{ height: 0, overflow: "hidden" }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
id="section-3-2"
|
||||
style={{ height: 0, overflow: "hidden" }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
id="section-3-3"
|
||||
style={{ height: 0, overflow: "hidden" }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Section 4 — TODO A10 */}
|
||||
<SectionPlaceholder
|
||||
id="section-4"
|
||||
title="4. Оценка участка"
|
||||
title="4. Инфраструктура"
|
||||
note="TODO A10: ScoreBreakdownPanel + ScoreBreakdownStackedBar + GateVerdictBanner + GeologyBlock / HydrologyBlock / GeotechRiskBlock"
|
||||
/>
|
||||
|
||||
{/* TODO A11: Section 5 — SeasonalWeatherBlock + air quality */}
|
||||
{/* Section 5 — TODO A11 */}
|
||||
<SectionPlaceholder
|
||||
id="section-5"
|
||||
title="5. Атмосфера / воздух"
|
||||
title="5. Свежесть"
|
||||
note="TODO A11: SeasonalWeatherBlock + air quality (reuse EnvironmentTab logic split на блоки)"
|
||||
/>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
interface AnalysisBreadcrumbProps {
|
||||
cadNum: string;
|
||||
}
|
||||
|
||||
export function AnalysisBreadcrumb({ cadNum }: AnalysisBreadcrumbProps) {
|
||||
return (
|
||||
<nav
|
||||
aria-label="Breadcrumb"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
flexWrap: "wrap",
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{/* SiteFinder root */}
|
||||
<Link
|
||||
href="/site-finder"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: "var(--fg-secondary)",
|
||||
textDecoration: "none",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
SiteFinder
|
||||
</Link>
|
||||
|
||||
<ChevronRight
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
style={{ color: "var(--fg-tertiary)", flexShrink: 0 }}
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
{/* Cad number */}
|
||||
<Link
|
||||
href={`/site-finder?selected=${encodeURIComponent(cadNum)}`}
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: "var(--fg-secondary)",
|
||||
textDecoration: "none",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
maxWidth: 260,
|
||||
}}
|
||||
title={cadNum}
|
||||
>
|
||||
{cadNum}
|
||||
</Link>
|
||||
|
||||
<ChevronRight
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
style={{ color: "var(--fg-tertiary)", flexShrink: 0 }}
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
{/* Current page */}
|
||||
<span
|
||||
aria-current="page"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "var(--fg-primary)",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
Анализ
|
||||
</span>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
254
frontend/src/components/site-finder/analysis/AnalysisSidebar.tsx
Normal file
254
frontend/src/components/site-finder/analysis/AnalysisSidebar.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface SubSection {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface NavSection {
|
||||
id: string;
|
||||
label: string;
|
||||
sub?: SubSection[];
|
||||
}
|
||||
|
||||
// ── Config ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const NAV_SECTIONS: NavSection[] = [
|
||||
{ id: "section-1", label: "1. Объект" },
|
||||
{ id: "section-2", label: "2. Земля и риски" },
|
||||
{
|
||||
id: "section-3",
|
||||
label: "3. Рынок",
|
||||
sub: [
|
||||
{ id: "section-3-1", label: "3.1 Настройки выборки" },
|
||||
{ id: "section-3-2", label: "3.2 Планировки" },
|
||||
{ id: "section-3-3", label: "3.3 Остатки и скорость" },
|
||||
],
|
||||
},
|
||||
{ id: "section-4", label: "4. Инфраструктура" },
|
||||
{ id: "section-5", label: "5. Свежесть" },
|
||||
];
|
||||
|
||||
// All section IDs in scroll order (for IntersectionObserver)
|
||||
const ALL_SECTION_IDS: string[] = NAV_SECTIONS.flatMap((s) =>
|
||||
s.sub ? [s.id, ...s.sub.map((sub) => sub.id)] : [s.id],
|
||||
);
|
||||
|
||||
// ── Component ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export function AnalysisSidebar() {
|
||||
const [activeId, setActiveId] = useState<string>(ALL_SECTION_IDS[0]);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
|
||||
// Scrollspy via IntersectionObserver
|
||||
useEffect(() => {
|
||||
const candidates = ALL_SECTION_IDS.map((id) =>
|
||||
document.getElementById(id),
|
||||
).filter((el): el is HTMLElement => el !== null);
|
||||
|
||||
if (candidates.length === 0) return;
|
||||
|
||||
// Track which sections are visible; pick topmost visible one
|
||||
const visible = new Set<string>();
|
||||
|
||||
observerRef.current = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
visible.add(entry.target.id);
|
||||
} else {
|
||||
visible.delete(entry.target.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Pick the topmost section that is currently visible
|
||||
const next = ALL_SECTION_IDS.find((id) => visible.has(id));
|
||||
if (next) setActiveId(next);
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
// Trigger when section top enters top 60% of viewport
|
||||
rootMargin: "-8px 0px -40% 0px",
|
||||
threshold: 0,
|
||||
},
|
||||
);
|
||||
|
||||
candidates.forEach((el) => observerRef.current!.observe(el));
|
||||
|
||||
return () => {
|
||||
observerRef.current?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
function handleAnchorClick(
|
||||
e: React.MouseEvent<HTMLAnchorElement>,
|
||||
targetId: string,
|
||||
) {
|
||||
e.preventDefault();
|
||||
const el = document.getElementById(targetId);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
setActiveId(targetId);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
style={{
|
||||
width: 240,
|
||||
flexShrink: 0,
|
||||
background: "var(--bg-card)",
|
||||
borderRight: "1px solid var(--border-card)",
|
||||
padding: "16px 12px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
position: "sticky",
|
||||
top: 56,
|
||||
height: "calc(100vh - 56px)",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{/* Section label */}
|
||||
<p
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.04em",
|
||||
color: "var(--fg-tertiary)",
|
||||
margin: "0 0 8px",
|
||||
padding: "0 4px",
|
||||
}}
|
||||
>
|
||||
Навигация
|
||||
</p>
|
||||
|
||||
{/* Nav items */}
|
||||
{NAV_SECTIONS.map((section) => {
|
||||
const isParentActive =
|
||||
activeId === section.id ||
|
||||
section.sub?.some((s) => s.id === activeId);
|
||||
|
||||
return (
|
||||
<div key={section.id}>
|
||||
<a
|
||||
href={`#${section.id}`}
|
||||
onClick={(e) => handleAnchorClick(e, section.id)}
|
||||
style={{
|
||||
display: "block",
|
||||
padding: "7px 10px",
|
||||
borderRadius: 8,
|
||||
fontSize: 13,
|
||||
fontWeight: isParentActive ? 600 : 400,
|
||||
color: isParentActive ? "var(--accent)" : "var(--fg-secondary)",
|
||||
background: isParentActive
|
||||
? "var(--accent-soft)"
|
||||
: "transparent",
|
||||
textDecoration: "none",
|
||||
transition: "background 100ms, color 100ms",
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{section.label}
|
||||
</a>
|
||||
|
||||
{/* Sub-sections */}
|
||||
{section.sub && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 12,
|
||||
marginTop: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{section.sub.map((sub) => {
|
||||
const isSubActive = activeId === sub.id;
|
||||
return (
|
||||
<a
|
||||
key={sub.id}
|
||||
href={`#${sub.id}`}
|
||||
onClick={(e) => handleAnchorClick(e, sub.id)}
|
||||
style={{
|
||||
display: "block",
|
||||
padding: "5px 10px",
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
fontWeight: isSubActive ? 600 : 400,
|
||||
color: isSubActive
|
||||
? "var(--accent)"
|
||||
: "var(--fg-tertiary)",
|
||||
background: isSubActive
|
||||
? "var(--accent-soft)"
|
||||
: "transparent",
|
||||
textDecoration: "none",
|
||||
transition: "background 100ms, color 100ms",
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{sub.label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Sources footer */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "auto",
|
||||
paddingTop: 16,
|
||||
borderTop: "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.04em",
|
||||
color: "var(--fg-tertiary)",
|
||||
margin: "0 0 6px",
|
||||
padding: "0 4px",
|
||||
}}
|
||||
>
|
||||
Источники
|
||||
</p>
|
||||
{[
|
||||
{ label: "Росреестр / ЕГРН", href: "https://rosreestr.gov.ru" },
|
||||
{ label: "НСПД", href: "https://nspd.gov.ru" },
|
||||
{ label: "2ГИС / OSM", href: "https://2gis.ru" },
|
||||
].map(({ label, href }) => (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
padding: "4px 4px",
|
||||
fontSize: 11,
|
||||
color: "var(--fg-tertiary)",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
<ExternalLink size={10} strokeWidth={1.5} />
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
104
frontend/src/components/site-finder/analysis/UserAvatar.tsx
Normal file
104
frontend/src/components/site-finder/analysis/UserAvatar.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Building2 } from "lucide-react";
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function getStoredOrgId(): string | null {
|
||||
// Guard against SSR — localStorage not available on server
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
return localStorage.getItem("gd_org_id");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function orgInitials(orgId: string): string {
|
||||
// Build a 2-letter monogram from org ID string
|
||||
const parts = orgId
|
||||
.toUpperCase()
|
||||
.replace(/[^A-ZА-Я0-9]/gu, " ")
|
||||
.split(" ")
|
||||
.filter(Boolean);
|
||||
if (parts.length === 0) return "??";
|
||||
if (parts.length === 1) return parts[0].slice(0, 2);
|
||||
return parts[0][0] + parts[1][0];
|
||||
}
|
||||
|
||||
// ── Component ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export function UserAvatar() {
|
||||
const [orgId, setOrgId] = useState<string | null>(null);
|
||||
|
||||
// Hydration-safe: read localStorage after mount
|
||||
useEffect(() => {
|
||||
setOrgId(getStoredOrgId());
|
||||
}, []);
|
||||
|
||||
const displayLabel = orgId ?? "Demo Org";
|
||||
const initials = orgId ? orgInitials(orgId) : "DO";
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
title={displayLabel}
|
||||
>
|
||||
{/* Avatar circle */}
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "50%",
|
||||
background: "var(--accent-soft)",
|
||||
border: "1px solid var(--border-card)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{orgId ? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
color: "var(--accent)",
|
||||
letterSpacing: "0.02em",
|
||||
}}
|
||||
>
|
||||
{initials}
|
||||
</span>
|
||||
) : (
|
||||
<Building2
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
style={{ color: "var(--accent)" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Org name — hidden on narrow viewports via maxWidth trick */}
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: "var(--fg-secondary)",
|
||||
maxWidth: 140,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{displayLabel}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue