ThemeController | CoUI
LogoCoUI

ThemeController

팔레트 색상 프리뷰가 있는 테마 스와치를 그리드로 표시하는 통합 테마 선택 컴포넌트

ThemeController#

ThemeController는 선택 가능한 테마 스와치를 줄바꿈 그리드로 표시하는 통합 테마 선택 컴포넌트입니다. 각 스와치는 테마의 팔레트 색상을 미리보기로 보여주며, 선택 시 테마 이름을 onThemeChanged로 전달합니다.

Live Preview#

사용 시기 (When to Use)#

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

  • 사용자가 여러 색상 테마 중 하나를 고르도록 할 때
  • 설정 화면에서 테마 미리보기와 함께 선택지를 제공할 때

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

  • Toggle: 라이트/다크 두 가지만 전환할 때
  • Select: 색상 프리뷰 없이 텍스트 목록으로 충분할 때

기본 사용법 (Basic Usage)#

ThemeController(
  selectedTheme: _selected,
  onThemeChanged: (name) => setState(() => _selected = name),
  themes: const [
    CoreThemeOption(
      name: 'Ocean',
      primaryColor: CoreColor.fromRGB(14, 165, 233),
      secondaryColor: CoreColor.fromRGB(99, 102, 241),
    ),
    CoreThemeOption(
      name: 'Forest',
      primaryColor: CoreColor.fromRGB(34, 197, 94),
      secondaryColor: CoreColor.fromRGB(132, 204, 22),
    ),
  ],
)
ThemeController(
  selectedTheme: _selected,
  onThemeChanged: (name) => setState(() => _selected = name),
  themes: const [
    CoreThemeOption(
      name: 'Ocean',
      primaryColor: CoreColor.fromRGB(14, 165, 233),
      secondaryColor: CoreColor.fromRGB(99, 102, 241),
    ),
    CoreThemeOption(
      name: 'Forest',
      primaryColor: CoreColor.fromRGB(34, 197, 94),
      secondaryColor: CoreColor.fromRGB(132, 204, 22),
    ),
  ],
)

빠른 오버라이드 (Chain)#

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

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

  @override
  State<ThemeControllerChainExample> createState() => _ThemeControllerChainExampleState();
}

class _ThemeControllerChainExampleState extends State<ThemeControllerChainExample> {
  String _selected = 'Ocean';

  @override
  Widget build(BuildContext context) {
    return ThemeController(
      selectedTheme: _selected,
      onThemeChanged: (name) => setState(() => _selected = name),
      themes: const [
        CoreThemeOption(
          name: 'Ocean',
          primaryColor: CoreColor.fromRGB(14, 165, 233),
          secondaryColor: CoreColor.fromRGB(99, 102, 241),
        ),
        CoreThemeOption(
          name: 'Forest',
          primaryColor: CoreColor.fromRGB(34, 197, 94),
          secondaryColor: CoreColor.fromRGB(132, 204, 22),
        ),
        CoreThemeOption(
          name: 'Sunset',
          primaryColor: CoreColor.fromRGB(249, 115, 22),
          secondaryColor: CoreColor.fromRGB(239, 68, 68),
        ),
      ],
    ).withStyle(
      const CoreThemeControllerStyle(
        buttonSize: CoreSize.size64,
        buttonRadius: CoreBorderRadius.all(CoreRadius.radius4),
        selectedBorderColor: CoreColor.token(CoreColors.primary),
        selectedBorderWidth: CoreStrokeWidth.stroke3,
      ),
    );
  }
}
class ThemeControllerChainExample extends StatefulComponent {
  const ThemeControllerChainExample({super.key});

  @override
  State<ThemeControllerChainExample> createState() => _ThemeControllerChainExampleState();
}

class _ThemeControllerChainExampleState extends State<ThemeControllerChainExample> {
  String _selected = 'Ocean';

  @override
  Component build(BuildContext context) {
    return ThemeController(
      selectedTheme: _selected,
      onThemeChanged: (name) => setState(() => _selected = name),
      themes: const [
        CoreThemeOption(
          name: 'Ocean',
          primaryColor: CoreColor.fromRGB(14, 165, 233),
          secondaryColor: CoreColor.fromRGB(99, 102, 241),
        ),
        CoreThemeOption(
          name: 'Forest',
          primaryColor: CoreColor.fromRGB(34, 197, 94),
          secondaryColor: CoreColor.fromRGB(132, 204, 22),
        ),
        CoreThemeOption(
          name: 'Sunset',
          primaryColor: CoreColor.fromRGB(249, 115, 22),
          secondaryColor: CoreColor.fromRGB(239, 68, 68),
        ),
      ],
    ).withStyle(
      const CoreThemeControllerStyle(
        buttonSize: CoreSize.size64,
        buttonRadius: CoreBorderRadius.all(CoreRadius.radius4),
        selectedBorderColor: CoreColor.token(CoreColors.primary),
        selectedBorderWidth: CoreStrokeWidth.stroke3,
      ),
    );
  }
}

Props / Parameters#

ThemeController#

속성타입기본값설명
themes List<CoreThemeOption> 필수 선택 가능한 테마 옵션 목록
selectedTheme String? null 현재 선택된 테마 이름
onThemeChanged void Function(String) 필수 테마 선택 핸들러 (옵션 이름 전달)
variant CoreThemeControllerVariant standard 시각 변형
themeControllerStyle CoreThemeControllerStyle? null 크롬 / 치수 스타일 오버라이드

CoreThemeOption#

속성타입기본값설명
name String 필수 테마 이름 (onThemeChanged 식별자)
primaryColorCoreColor필수주요 팔레트 색상 프리뷰
secondaryColor CoreColor? null 보조 팔레트 색상 프리뷰
accentColor CoreColor? null 액센트 팔레트 색상 프리뷰
neutralColor CoreColor? null 중립 팔레트 색상 프리뷰

스타일 시스템 (Style System)#

CoreThemeControllerStyle 필드#

필드타입설명
buttonSize double? Size of each theme button (logical px).
previewSize double? Size of each colour-preview circle (logical px).
buttonSpacing double? Spacing between adjacent theme buttons (logical px) — consumed as native Wrap.spacing / Wrap.runSpacing on Flutter and as the flex container gap CSS property on Web.
buttonRadius CoreBorderRadius? Corner radius of a theme button.
previewBorderRadius CoreBorderRadius? Corner radius of the colour-preview circle inside a swatch button. Defaults to a fully round dot — see [defaultPreviewBorderRadius].
selectedBorderColor CoreColor? Border colour of the selected theme button.
unselectedBorderColor CoreColor? Border colour of an unselected theme button.
selectedBorderWidth double? Border thickness of the selected theme button (logical px).
unselectedBorderWidth double? Border thickness of an unselected theme button (logical px).
clickableStyle CoreClickableStyle? Nested [CoreClickableStyle] slot for the composed per-swatch Clickable (press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.

CoreThemeControllerStyle 변형별 기본값 (CoreThemeControllerVariantStyle)#

필드standard
buttonBackgroundColorsurface

동작 스펙 (Behavior)#

인터랙션#

  • 선택: 스와치 클릭 시 onThemeChanged(name) 호출
  • 선택 표시: 선택된 스와치는 강조 보더(primary, 2px), 나머지는 기본 보더(outline, 1px)

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

✅ Do#

selectedTheme 값을 themes 목록의 name 과 정확히 일치시키기

ThemeController(
  selectedTheme: 'Ocean', // themes 안 CoreThemeOption.name 과 동일해야 함
  onThemeChanged: (name) => setState(() => _selected = name),
  themes: const [
    CoreThemeOption(name: 'Ocean', primaryColor: ...),
    CoreThemeOption(name: 'Forest', primaryColor: ...),
  ],
)

이유: 선택 표시는 selectedTheme 문자열과 각 CoreThemeOption.name 을 비교해 결정되므로, 값이 다르면 어떤 스와치에도 강조 보더가 표시되지 않습니다.


❌ Don't#

라이트/다크 2택 전환에 사용하지 않기

// ❌ 두 개짜리 선택지에 그리드 스와치 선택기 사용
ThemeController(
  themes: const [
    CoreThemeOption(name: 'Light', primaryColor: ...),
    CoreThemeOption(name: 'Dark', primaryColor: ...),
  ],
  selectedTheme: mode,
  onThemeChanged: (name) => setMode(name),
)

이유: ThemeController는 여러 팔레트 중 하나를 고르는 그리드 UI로 설계되어 있습니다. 두 상태만 전환할 때는 Toggle이 더 적은 탭 동작과 명확한 on/off 시맨틱을 제공합니다.

접근성 (Accessibility)#

스크린 리더#

  • Flutter: Semantics(selected: isSelected, button: true, label: themeName) 자동 적용
  • Web: 각 스와치 role="button" + aria-pressed + aria-label 적용

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

항목FlutterWeb
클래스명ThemeControllerThemeController
렌더 루트Wrap<div> (flex flex-wrap)
스와치 Container + BoxDecoration <div> + inline CSS border
  • Toggle: 라이트/다크 2가지 전환에 사용
  • Select: 색상 프리뷰 없는 텍스트 목록 선택