feat: IACE CE-Compliance Module — Normen, Risikobewertung, Production Lines
Major features: - 215 norms library with section references + Beuth URLs (A/B1/B2/C norms) - 173 hazard patterns with detail fields (scenario, trigger, harm, zone) - Deterministic pattern matching: Component × Lifecycle × Pattern cross-product - SIL/PL auto-calculation from S×E×P risk graph - Risk assessment table with editable S/E/P dropdowns - Production Line Dashboard with animated station flow (Running Dots) - IACE process flow + norms coverage on start page - Non-blocking cookie banner, ProcessFlow SSR fix - 104 Playwright E2E tests passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { LineDashboard } from '../../_types'
|
||||
|
||||
interface AggregatePanelProps {
|
||||
dashboard: LineDashboard
|
||||
}
|
||||
|
||||
const RISK_DOTS = [
|
||||
{ key: 'critical', label: 'Kritisch', dotColor: 'bg-red-500' },
|
||||
{ key: 'high', label: 'Hoch', dotColor: 'bg-orange-500' },
|
||||
{ key: 'medium', label: 'Mittel', dotColor: 'bg-yellow-500' },
|
||||
{ key: 'low', label: 'Niedrig', dotColor: 'bg-green-500' },
|
||||
]
|
||||
|
||||
export function AggregatePanel({ dashboard }: AggregatePanelProps) {
|
||||
const { line, stations, aggregate } = dashboard
|
||||
|
||||
const totalHazards = stations.reduce((sum, s) => sum + s.hazard_count, 0)
|
||||
const totalMitigations = stations.reduce((sum, s) => sum + s.mitigation_count, 0)
|
||||
const stationCount = stations.length
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||
{/* Title row */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{line.name}
|
||||
</h1>
|
||||
{line.description && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{line.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
<span>Erstellt: {new Date(line.created_at).toLocaleDateString('de-DE')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
<div className="flex flex-wrap items-center gap-6 mb-3">
|
||||
<StatPill label="Stationen" value={stationCount} />
|
||||
<StatPill label="Gefaehrdungen" value={totalHazards} />
|
||||
<StatPill label="Massnahmen" value={totalMitigations} />
|
||||
</div>
|
||||
|
||||
{/* Risk dots row */}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{RISK_DOTS.map((rd) => {
|
||||
const count = aggregate[rd.key] || 0
|
||||
return (
|
||||
<span key={rd.key} className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-300">
|
||||
<span className={`w-2.5 h-2.5 rounded-full ${rd.dotColor}`} />
|
||||
<span className="font-semibold">{count}</span>
|
||||
<span>{rd.label}</span>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatPill({ label, value }: { label: string; value: number }) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-sm">
|
||||
<span className="font-bold text-gray-900 dark:text-white">{value}</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">{label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { StationIcon } from './StationIcons'
|
||||
import { STATION_TYPES } from '../../_types'
|
||||
import type { StationDashboard } from '../../_types'
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
draft: 'bg-gray-100 text-gray-700',
|
||||
in_progress: 'bg-blue-100 text-blue-700',
|
||||
review: 'bg-yellow-100 text-yellow-700',
|
||||
approved: 'bg-green-100 text-green-700',
|
||||
archived: 'bg-gray-100 text-gray-500',
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
draft: 'Entwurf',
|
||||
in_progress: 'In Bearbeitung',
|
||||
review: 'In Pruefung',
|
||||
approved: 'Freigegeben',
|
||||
archived: 'Archiviert',
|
||||
}
|
||||
|
||||
const RISK_LEVELS = [
|
||||
{ key: 'critical', label: 'Kritisch', color: 'bg-red-500', text: 'text-red-700' },
|
||||
{ key: 'high', label: 'Hoch', color: 'bg-orange-500', text: 'text-orange-700' },
|
||||
{ key: 'medium', label: 'Mittel', color: 'bg-yellow-500', text: 'text-yellow-700' },
|
||||
{ key: 'low', label: 'Niedrig', color: 'bg-green-500', text: 'text-green-700' },
|
||||
]
|
||||
|
||||
interface StationCardProps {
|
||||
station: StationDashboard
|
||||
expanded: boolean
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
export function StationCard({ station, expanded, onToggle }: StationCardProps) {
|
||||
const stationType = STATION_TYPES[station.station.station_type]
|
||||
const bgColor = stationType?.bgColor || 'bg-gray-50'
|
||||
const accentColor = stationType?.color || '#6B7280'
|
||||
|
||||
const totalRisk = Object.values(station.risk_summary).reduce((a, b) => a + b, 0)
|
||||
const pctBar = station.completeness_pct
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-56 flex-shrink-0 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden shadow-sm hover:shadow-md transition-shadow`}
|
||||
>
|
||||
{/* Color accent bar */}
|
||||
<div className="h-1.5" style={{ backgroundColor: accentColor }} />
|
||||
|
||||
{/* Collapsed content */}
|
||||
<div className="p-4">
|
||||
{/* Icon + name */}
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div
|
||||
className={`w-10 h-10 ${bgColor} dark:bg-opacity-20 rounded-lg flex items-center justify-center flex-shrink-0`}
|
||||
style={{ color: accentColor }}
|
||||
>
|
||||
<StationIcon type={station.station.station_type} size={22} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold text-gray-900 dark:text-white truncate">
|
||||
{station.station.station_label}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{station.project_name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hazard count */}
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 mb-2">
|
||||
{station.hazard_count} Gefaehrdungen
|
||||
</div>
|
||||
|
||||
{/* Completeness bar */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className="h-2 rounded-full transition-all"
|
||||
style={{
|
||||
width: `${pctBar}%`,
|
||||
backgroundColor: accentColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400 w-8 text-right">
|
||||
{pctBar}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* SIL / PL */}
|
||||
{(station.sil_max || station.pl_max) && (
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{station.sil_max && <span>SIL {station.sil_max}</span>}
|
||||
{station.sil_max && station.pl_max && <span>|</span>}
|
||||
{station.pl_max && <span>PL {station.pl_max}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toggle button */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full text-left text-xs text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 font-medium flex items-center gap-1"
|
||||
>
|
||||
{expanded ? 'Weniger anzeigen' : 'Details anzeigen'}
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 transition-transform ${expanded ? 'rotate-90' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Expanded content */}
|
||||
{expanded && (
|
||||
<div className="px-4 pb-4 border-t border-gray-100 dark:border-gray-700 pt-3 space-y-3">
|
||||
{/* Risk breakdown */}
|
||||
<div className="space-y-1.5">
|
||||
{RISK_LEVELS.map((level) => {
|
||||
const count = station.risk_summary[level.key] || 0
|
||||
const pct = totalRisk > 0 ? Math.round((count / totalRisk) * 100) : 0
|
||||
return (
|
||||
<div key={level.key} className="flex items-center gap-2">
|
||||
<span className={`text-[10px] font-medium w-12 ${level.text}`}>{level.label}</span>
|
||||
<div className="flex-1 bg-gray-100 dark:bg-gray-700 rounded-full h-2.5 overflow-hidden">
|
||||
<div className={`${level.color} h-2.5 rounded-full`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className="text-xs font-bold text-gray-700 dark:text-gray-300 w-6 text-right">{count}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Mitigation count */}
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400">Massnahmen</span>
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">{station.mitigation_count}</span>
|
||||
</div>
|
||||
|
||||
{/* Status badge */}
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400">Status</span>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${STATUS_COLORS[station.status] || STATUS_COLORS.draft}`}>
|
||||
{STATUS_LABELS[station.status] || station.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Link to project */}
|
||||
<Link
|
||||
href={`/sdk/iace/${station.station.project_id}`}
|
||||
className="block text-center text-xs font-medium text-purple-600 dark:text-purple-400 hover:text-purple-700 bg-purple-50 dark:bg-purple-900/20 rounded-lg py-2 transition-colors"
|
||||
>
|
||||
Zum Projekt →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import React from 'react'
|
||||
|
||||
interface StationIconProps {
|
||||
type: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
export function StationIcon({ type, size = 24 }: StationIconProps) {
|
||||
const s = size
|
||||
const sw = 1.5
|
||||
|
||||
switch (type) {
|
||||
case 'press':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Ram pressing down */}
|
||||
<rect x="7" y="2" width="10" height="4" rx="1" />
|
||||
<line x1="12" y1="6" x2="12" y2="12" />
|
||||
<path d="M6 12h12v3H6z" />
|
||||
<line x1="12" y1="12" x2="12" y2="10" strokeWidth={2.5} />
|
||||
{/* Base block */}
|
||||
<rect x="5" y="18" width="14" height="4" rx="1" />
|
||||
{/* Workpiece */}
|
||||
<rect x="9" y="15" width="6" height="3" rx="0.5" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'robot':
|
||||
case 'cobot':
|
||||
case 'collaborative_robot':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Base */}
|
||||
<rect x="8" y="19" width="8" height="3" rx="1" />
|
||||
{/* Lower arm */}
|
||||
<line x1="12" y1="19" x2="8" y2="13" />
|
||||
{/* Joint */}
|
||||
<circle cx="8" cy="13" r="1.5" />
|
||||
{/* Upper arm */}
|
||||
<line x1="8" y1="13" x2="15" y2="7" />
|
||||
{/* Wrist joint */}
|
||||
<circle cx="15" cy="7" r="1.5" />
|
||||
{/* Gripper */}
|
||||
<line x1="15" y1="7" x2="18" y2="4" />
|
||||
<line x1="18" y1="4" x2="19" y2="3" />
|
||||
<line x1="18" y1="4" x2="19" y2="5" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'conveyor':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Belt top */}
|
||||
<line x1="3" y1="14" x2="21" y2="14" />
|
||||
{/* Belt bottom */}
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
{/* Left roller */}
|
||||
<circle cx="5" cy="16" r="2" />
|
||||
{/* Right roller */}
|
||||
<circle cx="19" cy="16" r="2" />
|
||||
{/* Flow arrows */}
|
||||
<path d="M8 10l3-2 3 2" />
|
||||
<path d="M11 8v-2" />
|
||||
{/* Package on belt */}
|
||||
<rect x="9" y="10" width="6" height="4" rx="0.5" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'assembly':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Gear */}
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<circle cx="12" cy="12" r="1.5" />
|
||||
{/* Gear teeth */}
|
||||
<line x1="12" y1="3" x2="12" y2="6" />
|
||||
<line x1="12" y1="18" x2="12" y2="21" />
|
||||
<line x1="3" y1="12" x2="6" y2="12" />
|
||||
<line x1="18" y1="12" x2="21" y2="12" />
|
||||
<line x1="5.6" y1="5.6" x2="7.8" y2="7.8" />
|
||||
<line x1="16.2" y1="16.2" x2="18.4" y2="18.4" />
|
||||
<line x1="5.6" y1="18.4" x2="7.8" y2="16.2" />
|
||||
<line x1="16.2" y1="7.8" x2="18.4" y2="5.6" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'milling':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Spindle */}
|
||||
<rect x="10" y="2" width="4" height="6" rx="1" />
|
||||
{/* Cutter head */}
|
||||
<circle cx="12" cy="11" r="3" />
|
||||
{/* Rotation arc */}
|
||||
<path d="M7 11a5 5 0 0 1 2.5-4.3" strokeDasharray="2 2" />
|
||||
<path d="M17 11a5 5 0 0 0-2.5-4.3" strokeDasharray="2 2" />
|
||||
{/* Workpiece / table */}
|
||||
<rect x="4" y="17" width="16" height="3" rx="1" />
|
||||
<rect x="8" y="14" width="8" height="3" rx="0.5" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'turning':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Chuck / rotating workpiece */}
|
||||
<circle cx="9" cy="12" r="5" />
|
||||
<circle cx="9" cy="12" r="2" />
|
||||
{/* Tool holder */}
|
||||
<line x1="16" y1="12" x2="14" y2="12" />
|
||||
<path d="M16 9v6l4-1v-4z" />
|
||||
{/* Rotation arrow */}
|
||||
<path d="M5 5a8 8 0 0 1 4 1" />
|
||||
<path d="M5 5l1.5 1.5L4.5 7" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'welding':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Torch body */}
|
||||
<line x1="6" y1="4" x2="12" y2="14" />
|
||||
<path d="M4 3h4l-2 3z" />
|
||||
{/* Weld point */}
|
||||
<circle cx="12" cy="16" r="1" fill="currentColor" />
|
||||
{/* Sparks */}
|
||||
<line x1="12" y1="16" x2="15" y2="13" />
|
||||
<line x1="12" y1="16" x2="16" y2="15" />
|
||||
<line x1="12" y1="16" x2="14" y2="19" />
|
||||
<line x1="12" y1="16" x2="9" y2="19" />
|
||||
{/* Workpiece */}
|
||||
<rect x="3" y="19" width="18" height="3" rx="1" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'inspection':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Magnifying glass */}
|
||||
<circle cx="10" cy="10" r="6" />
|
||||
<line x1="14.5" y1="14.5" x2="20" y2="20" strokeWidth={2} />
|
||||
{/* Checkmark inside */}
|
||||
<path d="M7.5 10l2 2 3.5-4" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'packaging':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Box */}
|
||||
<path d="M3 8l9-5 9 5v10l-9 5-9-5z" />
|
||||
<line x1="12" y1="3" x2="12" y2="23" />
|
||||
<line x1="3" y1="8" x2="12" y2="13" />
|
||||
<line x1="21" y1="8" x2="12" y2="13" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'motor':
|
||||
case 'electric_motor':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Motor body circle */}
|
||||
<circle cx="12" cy="12" r="8" />
|
||||
{/* Lightning bolt */}
|
||||
<path d="M13 6l-3 6h4l-3 6" strokeWidth={2} />
|
||||
{/* Shaft */}
|
||||
<line x1="20" y1="12" x2="23" y2="12" strokeWidth={2} />
|
||||
</svg>
|
||||
)
|
||||
|
||||
case 'rotary_transfer':
|
||||
case 'rotary_transfer_machine':
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* Circular path */}
|
||||
<circle cx="12" cy="12" r="8" strokeDasharray="4 2" />
|
||||
{/* Center */}
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
{/* Station dots around circle */}
|
||||
<circle cx="12" cy="4" r="1.5" fill="currentColor" />
|
||||
<circle cx="19" cy="8" r="1.5" fill="currentColor" />
|
||||
<circle cx="19" cy="16" r="1.5" fill="currentColor" />
|
||||
<circle cx="12" cy="20" r="1.5" fill="currentColor" />
|
||||
<circle cx="5" cy="16" r="1.5" fill="currentColor" />
|
||||
<circle cx="5" cy="8" r="1.5" fill="currentColor" />
|
||||
{/* Rotation arrow */}
|
||||
<path d="M16 3.5l-1 2h2z" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { TransferInfo } from '../../_types'
|
||||
|
||||
interface TransferLineProps {
|
||||
transfer: TransferInfo
|
||||
color: string
|
||||
}
|
||||
|
||||
export function TransferLine({ transfer, color }: TransferLineProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-20 flex-shrink-0 py-4">
|
||||
<style>{`
|
||||
@keyframes iace-running-dots {
|
||||
0% { stroke-dashoffset: 12; }
|
||||
100% { stroke-dashoffset: 0; }
|
||||
}
|
||||
.iace-transfer-dots {
|
||||
animation: iace-running-dots 0.8s linear infinite;
|
||||
}
|
||||
`}</style>
|
||||
<svg width="80" height="32" viewBox="0 0 80 32" className="overflow-visible">
|
||||
{/* Background line */}
|
||||
<line
|
||||
x1="0"
|
||||
y1="16"
|
||||
x2="80"
|
||||
y2="16"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeOpacity="0.3"
|
||||
/>
|
||||
{/* Animated running dots */}
|
||||
<line
|
||||
x1="0"
|
||||
y1="16"
|
||||
x2="80"
|
||||
y2="16"
|
||||
stroke={color}
|
||||
strokeWidth="2.5"
|
||||
strokeDasharray="4 8"
|
||||
className="iace-transfer-dots"
|
||||
/>
|
||||
{/* Arrowhead */}
|
||||
<polygon
|
||||
points="74,11 80,16 74,21"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
{/* Label */}
|
||||
{transfer.label && (
|
||||
<span className="text-[10px] text-gray-500 dark:text-gray-400 mt-1 text-center leading-tight max-w-[80px] truncate">
|
||||
{transfer.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user