Tracker | CoUI
LogoCoUI

Tracker

여러 세그먼트로 구성된 진행 상태를 시각적으로 추적하는 컴포넌트

Tracker#

여러 세그먼트로 구성된 상태를 색상으로 시각화하는 컴포넌트입니다. 업타임 모니터링, 습관 추적, 스트릭 표시 등에 활용됩니다.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 서비스 업타임/다운타임 이력을 색상 바로 표시할 때
  • 습관 추적, 스트릭, 연속 기록을 시각화할 때

대신 다른 컴포넌트를 사용하세요:

  • Progress: 단일 진행률 표시
  • Stat: 숫자 기반 통계 표시

기본 사용법 (Basic Usage)#

Tracker(
  segments: [
    CoreTrackerSegment(status: CoreTrackerStatus.fine, tooltip: 'OK'),
    CoreTrackerSegment(status: CoreTrackerStatus.warning, tooltip: 'Slow'),
    CoreTrackerSegment(status: CoreTrackerStatus.critical, tooltip: 'Down'),
    CoreTrackerSegment(status: CoreTrackerStatus.unknown, tooltip: 'N/A'),
  ],
)
Tracker(
  segments: [
    CoreTrackerSegment(status: CoreTrackerStatus.fine, tooltip: 'OK'),
    CoreTrackerSegment(status: CoreTrackerStatus.warning, tooltip: 'Slow'),
    CoreTrackerSegment(status: CoreTrackerStatus.critical, tooltip: 'Down'),
    CoreTrackerSegment(status: CoreTrackerStatus.unknown, tooltip: 'N/A'),
  ],
)

빠른 오버라이드 (Chain)#

이미 만든 Tracker 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class TrackerChainExample extends StatefulWidget {
  const TrackerChainExample({super.key});

  @override
  State<TrackerChainExample> createState() => _TrackerChainExampleState();
}

class _TrackerChainExampleState extends State<TrackerChainExample> {
  @override
  Widget build(BuildContext context) {
    return Tracker(
      segments: [
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.warning),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.critical),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.unknown),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.warning),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
      ],
    ).withStyle(
      const CoreTrackerStyle(
        itemHeight: CoreSpace.space48,
        itemSpacing: CoreSpace.space4,
        endRadius: CoreRadius.radius8,
      ),
    );
  }
}
class TrackerChainExample extends StatefulComponent {
  const TrackerChainExample({super.key});

  @override
  State<TrackerChainExample> createState() => _TrackerChainExampleState();
}

class _TrackerChainExampleState extends State<TrackerChainExample> {
  @override
  Component build(BuildContext context) {
    return Tracker(
      segments: [
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.warning),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.critical),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.unknown),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.warning),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
        CoreTrackerSegment(status: CoreTrackerStatus.fine),
      ],
    ).withStyle(
      const CoreTrackerStyle(
        itemHeight: CoreSpace.space48,
        itemSpacing: CoreSpace.space4,
        endRadius: CoreRadius.radius8,
      ),
    );
  }
}

Props / Parameters#

속성타입기본값설명
segments List<CoreTrackerSegment> 필수 세그먼트 데이터 목록
roundedbooltrue모서리 둥글기 적용
trackerStyle CoreTrackerStyle? null 인스턴스 chrome/dimensional override

CoreTrackerStyle 필드#

필드타입설명
itemHeight double? Height of each tracker segment (logical px).
itemSpacing double? Horizontal spacing (logical px, pre-scaling) between adjacent tracker segments. Flows directly into Row.spacing (Flutter native paint-API argument, 3.27+) on Flutter and the outer flex row's CSS gap inline rule on Web — both are native paint-API consumers that take a raw double, so this slot stays scalar ( double? ) rather than the nested CoreGapStyle shape used for Gap slots. Falls back to [defaultItemSpacing] when null.
endRadius double? Border-radius applied to the outer ends of the segment row when widget.rounded == true (logical px).
segmentColor CoreColor? Optional global override applied to every segment, regardless of the segment's [CoreTrackerStatus]. When null (the default) the resolver falls back to the per-status colour in [defaultsByStatus].

CoreTrackerSegment#

속성타입설명
status CoreTrackerStatus 상태 (fine, warning, critical, unknown)
tooltipString?호버 시 툴팁 텍스트

CoreTrackerStatus 색상#

상태색상
finesuccess (green)
warningwarning (orange)
criticalerror (red)
unknownoutlineVariant (gray)

사용 가이드라인 (Usage Guidelines)#

✅ Do#

세그먼트마다 의미 있는 tooltip 지정

CoreTrackerSegment(status: CoreTrackerStatus.critical, tooltip: '다운타임 발생')

이유: tooltip을 생략하면 접근 가능한 이름이 로컬라이즈되지 않은 CoreTrackerStatus enum 이름(critical 등)으로 대체되어, 스크린 리더 사용자에게 상태 의미가 전달되지 않습니다.


❌ Don't#

요약 정보 없이 Tracker 단독 배치 금지

// ❌ "12개 중 3개 위험" 같은 요약 없이 Tracker 만 배치
Tracker(segments: uptimeSegments)

이유: 세그먼트는 hover 로만 열리는 툴팁을 가진 비인터랙티브 요소라 키보드 사용자는 개별 상태를 확인할 방법이 없습니다. 항목 수·위험 개수 같은 요약 정보는 호출자가 별도 텍스트로 제공해야 합니다.

접근성 (Accessibility)#

역할 / Semantics#

양 플랫폼 모두 그룹과 세그먼트 둘 다에 이름을 붙입니다. Flutter 는 트랙 전체를 Semantics(container: true, label: ...) 로 감싸고, Web 루트 <div> 는 같은 값을 aria-label 로 내보냅니다(CouiLocalizations.trackerLabel — 기본 Tracker, 한국어 트래커; 라벨일 뿐 role 은 아닙니다). 세그먼트마다도 aria-label 이 붙는데, 값은 tooltip ?? status.name — 호출자가 tooltip 을 지정하면 그 텍스트가, 지정하지 않으면 CoreTrackerStatus 의 enum 이름(fine / warning / critical / unknown)이 대신 쓰입니다(Flutter 의 Tooltip(message: tooltip ?? status.name) 과 같은 소스 · 같은 폴백). role 이 없는 <div> 위의 aria-label 이라 그룹/세그먼트 라벨 모두 일부 스크린 리더에서는 무시될 수 있습니다. 실제 role 을 가진 요소는 합성된 Tooltip 패널(role="tooltip") 하나뿐입니다.

키보드#

처리하는 키가 없습니다.

포커스#

포커스를 받는 요소가 하나도 없습니다 — 세그먼트는 비인터랙티브 Container / <div> 입니다. Tab 으로 트래커에 진입할 수 없으므로 hover 로만 열리는 세그먼트 툴팁을 키보드로는 볼 수 없습니다(Web mouseenter / mouseleave, Flutter HoverMouseRegion).

스크린 리더#

Flutter 는 로컬라이즈된 "트래커" 한 단어를 말하는 컨테이너 하나로 읽히고 개별 세그먼트 상태는 읽히지 않습니다 — 툴팁 메시지는 hover 시점에 마운트되는 오버레이 안에 있고, 합성된 Tooltip 역시 Semantics(container: true) 만 감쌉니다. Web 은 그룹 이름에 더해 세그먼트마다도 aria-label 이 있지만, role 이 없는 <div> 위의 aria-label 이라 스크린 리더가 실제로 읽어 줄지는 구현체마다 다릅니다.

알려진 제약#

  • tooltip 을 지정하지 않은 세그먼트의 접근 가능한 이름은 로컬라이즈되지 않은 CoreTrackerStatus enum 이름으로 대체됩니다(Flutter/Web 동일).
  • role="img" / list / meter 같은 역할이 없고, 트랙 전체를 대신할 텍스트 요약도 없습니다.
  • 세그먼트 툴팁에 키보드로 접근할 방법이 없습니다.

상태가 색과 툴팁으로만 구분되므로, 트래커가 전달하는 정보를 화면 밖에서도 알아야 한다면(예: "12개 중 3개 위험") 그 요약 문장을 소비자가 직접 제공해야 합니다.

감속 모션 · 고대비 · 강제 색상 · 최소 터치 타겟처럼 전 컴포넌트에 공통으로 걸리는 축은 전역 접근성 축에서 다룹니다.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명TrackerTracker
렌더링 Row + Expanded + Container <div> flex + flex-1
둥글기 BorderRadius.horizontal + theme.radiusField rounded-l-${radius} / rounded-r-${radius} ( CoreTrackerTokens.endRadius )
툴팁Tooltip 위젯Tooltip 컴포넌트
상태 → 색상 CoreTrackerTokenscolorScheme.resolve(...) CoreTrackerTokensbg-${token} Tailwind
  • Progress: 단일 진행률 바
  • Stat: 숫자 기반 통계 카드