WindowPanel | CoUI
LogoCoUI

WindowPanel

타이틀 바 + 컨트롤(최소화/최대화/닫기) + 콘텐츠 영역을 가진 윈도우 스타일 패널

WindowPanel#

데스크탑 앱 창을 연상시키는 윈도우 스타일 패널입니다. 타이틀 바 · 선택적 액션 슬롯 · 최소화/최대화/닫기 컨트롤 버튼 · 콘텐츠 영역으로 구성되며, 단독으로도 쓸 수 있고 더 큰 윈도우 매니저(WindowNavigator)의 building block으로도 쓸 수 있습니다.

Live Preview#

사용 시기 (When to Use)#

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

  • 문서 내 데스크탑 UI 샘플, 툴바 / 사이드바 위에 얹는 보조 패널, 개발 도구형 UI에서 창 외형이 필요할 때
  • Window / WindowNavigator 를 구현할 때의 프레임 쉘

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

  • 모달: Dialog / Drawer
  • 단순 카드: Card

기본 사용법 (Basic Usage)#

WindowPanel(
  title: Text('Window Title'),
  minimizable: true,
  maximizable: true,
  onClose: () {},
  child: Text('Window content'),
)

Web도 동일한 생성자로 사용합니다 — child/title/actions 타입만 다릅니다.

빠른 오버라이드 (Chain)#

이미 만든 WindowPanel 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 == CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

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

  @override
  State<WindowPanelChainExample> createState() => _WindowPanelChainExampleState();
}

class _WindowPanelChainExampleState extends State<WindowPanelChainExample> {
  bool _minimized = false;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 320,
      child:
          WindowPanel(
                title: const Text('Window Title'),
                minimizable: true,
                maximizable: true,
                isMinimized: _minimized,
                onMinimize: () => setState(() => _minimized = !_minimized),
                onMaximize: () => setState(() => _minimized = false),
                onClose: () => setState(() => _minimized = false),
                child: const Align(
                  alignment: Alignment.centerLeft,
                  child: Text('Window content goes here.'),
                ),
              )
              .withStyle(
                const CoreWindowPanelStyle(
                  titleBarColor: CoreColor.token(CoreColors.tertiaryContainer),
                  titleBarHeight: CoreSpace.space40,
                  contentPadding: CoreEdgeInsets.all(CoreSpace.space24),
                ),
              )
              .radius16,
    );
  }
}
class WindowPanelChainExample extends StatefulComponent {
  const WindowPanelChainExample({super.key});

  @override
  State<WindowPanelChainExample> createState() => _WindowPanelChainExampleState();
}

class _WindowPanelChainExampleState extends State<WindowPanelChainExample> {
  bool _minimized = false;

  @override
  Component build(BuildContext context) {
    return div(
      [
        WindowPanel(
              title: Text('Window Title'),
              minimizable: true,
              maximizable: true,
              isMinimized: _minimized,
              onMinimize: () => setState(() => _minimized = !_minimized),
              onMaximize: () => setState(() => _minimized = false),
              onClose: () => setState(() => _minimized = false),
              child: Text('Window content goes here.'),
            )
            .withStyle(
              const CoreWindowPanelStyle(
                titleBarColor: CoreColor.token(CoreColors.tertiaryContainer),
                titleBarHeight: CoreSpace.space40,
                contentPadding: CoreEdgeInsets.all(CoreSpace.space24),
              ),
            )
            .radius16,
      ],
      styles: Styles(
        // `max-width: 100%` clamps the fixed demo width inside narrow
        // preview panes — mirrors how the Flutter twin's `SizedBox`
        // width is clamped by its parent constraints.
        raw: const {'width': '${320 / 16}rem', 'max-width': '100%'},
      ),
    );
  }
}

Props / Parameters#

이름타입기본값설명
title Widget? / Component? null 타이틀 바에 표시할 위젯/컴포넌트
child Widget? / Component? null 콘텐츠 영역 (isMinimized=true면 숨김)
actions Widget? / Component? null 타이틀 바 우측 액션 슬롯 (기본 컨트롤 버튼 앞에 배치)
closablebooltrue닫기 버튼 표시
minimizable bool false 최소화 버튼 표시
maximizable bool false 최대화 버튼 표시
onCloseCoreVoidCallback?null닫기 버튼 콜백
onMinimize CoreVoidCallback? null 최소화 버튼 콜백
onMaximize CoreVoidCallback? null 최대화 버튼 콜백
isMinimized bool false 콘텐츠 영역을 숨김 (타이틀 바만 남김)
windowPanelStyle CoreWindowPanelStyle? null 외부 프레임 코너 반경 / 타이틀 바 배경 등 chrome 오버라이드 (단일 진입점)

타이틀 바 제스처 콜백#

Window / WindowNavigator 가 이 패널을 드래그 이동 · 더블클릭 최대화에 쓰는 훅입니다. 좌표는 플랫폼 무관 CoreOffset 로 전달됩니다.

이름타입기본값설명
onTitleBarDragStart CoreTitleBarDragStartCallback? null 드래그 시작 — (localPosition, globalPosition)
onTitleBarDragUpdate CoreTitleBarDragUpdateCallback? null 드래그 이동 — (delta, globalPosition)
onTitleBarDragEnd CoreTitleBarDragEndCallback? null 드래그 종료
onTitleBarDragCancel CoreTitleBarDragEndCallback? null 드래그 취소
onTitleBarDoubleTap CoreVoidCallback? null 타이틀 바 더블클릭 (최대화 / 복원 토글용)

스타일 시스템 — CoreWindowPanelStyle#

패널의 모든 chrome / dimensional / nested-slot 오버라이드는 windowPanelStyle 단일 슬롯으로 흐릅니다.

CoreWindowPanelStyle 필드#

필드타입설명
borderRadius CoreBorderRadius? Corner radius applied to the outer window frame.
titleBarColor CoreColor? Custom title bar background colour.
titleBarHeight double? Custom title bar height (pre-scaling).
titleBarPadding CoreEdgeInsets? Custom padding inside the title bar (pre-scaling).
contentPadding CoreEdgeInsets? Custom content area padding (pre-scaling).
controlSpacing double? Custom spacing between adjacent control buttons (pre-scaling). Consumed natively by Row(spacing: …) on Flutter and CSS gap on Web so the N control buttons sit on a single flex strip with equal inter-sibling spacing.
containerBorderWidth double? Custom container border width (logical/CSS px) — drives both the outer frame border and the inner title-bar radius shrink.
shadowColor CoreColor? Custom ambient drop-shadow base colour (e.g. a tinted shadow).
shadow List<CoreShadowLayer>? Custom frame drop-shadow layer geometry (tinted by [shadowColor]).
surfaceColor CoreColor? Custom outer window frame background colour.
borderColor CoreColor? Custom border colour for the outer frame + title bar divider.
titleStyle CoreTextStyle? Custom title text typography (role + colour + weight overlay).
contentStyle CoreTextStyle? Custom content area typography (role + colour).
controlButtonStyle CoreButtonStyle? Custom chrome for the non-close control buttons (minimize / maximize) — nested Button slot, raw-forwarded so the button's own resolver settles the final chrome.
closeButtonStyle CoreButtonStyle? Custom chrome for the close control button — nested Button slot.
controlIconStyle CoreIconStyle? Custom control button icon geometry (size; colour inherits the button's per-state foreground) — nested Icon slot.

Theming#

CoreComponentTheme.windowPanelstyle 슬롯으로 기본값 오버라이드:

CoreComponentTheme(
  windowPanel: CoreWindowPanelTheme(
    style: CoreWindowPanelStyle(
      borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
      titleBarColor: CoreColor.token(CoreColors.surfaceContainerHigh),
    ),
  ),
)

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

✅ Do#

closable/minimizable/maximizable 을 켤 때는 대응 콜백도 같이 준다

WindowPanel(
  title: const Text('Settings'),
  onClose: () => closePanel(),
)

closable 은 기본값이 true 라 따로 켤 필요가 없지만, onClose 를 주지 않으면 버튼은 여전히 그려지되 비활성(Web disabled + tabindex="-1", Flutter enabled: false) 상태로 렌더링됩니다.


❌ Don't#

플래그만 켜고 콜백은 빠뜨리지 않기

// ❌ minimizable/maximizable 만 켜고 onMinimize/onMaximize 는 안 줌
WindowPanel(
  title: const Text('Settings'),
  minimizable: true,
  maximizable: true,
)

컨트롤 버튼은 closable/minimizable/maximizable 로 표시 여부만 결정되고, 클릭 가능 여부는 대응하는 onClose/onMinimize/onMaximize 콜백 존재 여부로 갈립니다 — 콜백 없이 플래그만 켜면 눌러도 반응 없는 버튼이 보입니다.

✅ Do#

닫기 버튼만 강조하려면 closeButtonStyle 슬롯 하나만 건드리기

WindowPanel(
  title: const Text('Settings'),
  onClose: handleClose,
  windowPanelStyle: CoreWindowPanelStyle(
    closeButtonStyle: CoreButtonStyle(
      hoverForegroundColor: CoreColor.token(CoreColors.error),
    ),
  ),
)

최소화/최대화 버튼은 별도의 controlButtonStyle 슬롯이 담당하므로, closeButtonStyle 만 바꾸면 닫기 버튼에만 영향을 줍니다.


❌ Don't#

하나의 슬롯이 모든 컨트롤 버튼을 담당한다고 가정하지 않기

// ❌ controlButtonStyle 은 닫기 버튼에는 적용되지 않는다
WindowPanel(
  minimizable: true,
  maximizable: true,
  onClose: handleClose,
  windowPanelStyle: CoreWindowPanelStyle(
    controlButtonStyle: CoreButtonStyle(
      foregroundColor: CoreColor.token(CoreColors.error),
    ),
  ),
)

controlButtonStyle 은 최소화/최대화 버튼에만 적용되고 닫기 버튼은 별도의 closeButtonStyle 을 따로 봅니다 — 하나만 바꾸면 닫기 버튼은 기본 chrome 그대로 남습니다.

접근성 (Accessibility)#

역할 / Semantics#

루트가 두 플랫폼에서 다르게 발표됩니다. Web 은 루트 <div>role="dialog" + aria-label={CouiLocalizations.windowLabel} 을 붙이고, Flutter 는 역할 없이 Semantics(container: true, label: CoUILocalizations.windowLabel) 만 감쌉니다 — 둘 다 활성 로케일에서 이름을 가져오므로(기본 Window, 한국어 ) 값 자체는 같습니다. Web 쪽에도 aria-modal / aria-expanded / aria-controls 는 없습니다.

컨트롤 버튼(최소화 / 최대화 / 닫기)만 제대로 된 시맨틱을 갖습니다 — Flutter 는 Semantics(button: true, label: ...), Web 은 네이티브 <button type="button"> + aria-label. 타이틀 바 · 타이틀 텍스트 · 컨트롤 행 · 콘텐츠 영역은 양쪽 모두 rolearia-* 도 내보내지 않습니다.

키보드#

동작
Enter포커스된 컨트롤 버튼(최소화 / 최대화 / 닫기) 활성화
Space포커스된 컨트롤 버튼 활성화

패널 자체는 어떤 키도 처리하지 않습니다. 위 두 키는 합성된 컨트롤 버튼(Flutter Clickable, Web 네이티브 <button>)이 제공하는 것입니다. 타이틀 바 드래그 이동과 더블클릭 최대화는 포인터 전용이고, Escape 는 양 플랫폼 어디에도 핸들러가 없어 아무 동작도 하지 않습니다.

포커스#

패널 루트는 양 플랫폼 모두 포커스를 받지 않습니다 — Flutter 에 FocusNode / FocusScope 가 없고, Web 루트 <div>tabindex 가 없습니다. 포커스 트랩도, 열릴 때의 초기 포커스도, 닫힐 때의 포커스 복원도, 배경에 대한 inert 처리도 없습니다 — Web 이 role="dialog" 를 선언하는데도 그렇습니다.

포커스를 받는 것은 활성화된 컨트롤 버튼뿐입니다. 버튼이 비활성이면(콜백 없음) Web 은 disabled + tabindex="-1" + aria-disabled="true", Flutter 는 enabled: false 로 양쪽 다 Tab 순서에서 빠집니다. Web 콘텐츠 영역은 스크롤 컨테이너지만 tabindex 가 없어 키보드로 스크롤할 수 없습니다.

스크린 리더#

Web 에서는 루트가 로케일화된 "Window"/"창" 이름의 dialog 로 발표되지만, aria-modal 도 포커스 트랩도 없어 탐색이 전혀 제한되지 않습니다. Flutter 에서는 같은 이름이 붙은 평범한 컨테이너로 읽힙니다.

컨트롤 버튼 이름은 양 플랫폼 모두 활성 로케일에서 가져오므로 한국어에서 "최소화 / 최대화 / 닫기" 로 동일하게 읽힙니다. 타이틀 · actions 슬롯 · 콘텐츠는 그룹 역할이나 라벨 없이 일반 텍스트로 읽히고, 최소화 동작은 아무것도 발표하지 않습니다.

알려진 제약#

  • 이동 · 최대화/복원 · 최소화의 키보드 경로가 없습니다. 드래그와 더블클릭 최대화는 포인터 전용이라, 키보드만 쓰는 사용자는 컨트롤 버튼에만 닿을 수 있습니다.
  • Escape 로 닫히지 않습니다. 닫기가 필요하면 호출자가 상위에서 키 핸들러를 직접 달아야 합니다.
  • Web 의 role="dialog" 는 동작보다 많이 약속합니다. 모달로 다뤄야 한다면 포커스 트랩 · 초기 포커스 · 포커스 복원 · 배경 비활성화를 호출자가 구현하거나, 그런 보장이 필요한 자리에는 Dialog 를 쓰세요.
  • isMinimized 상태가 보조 기술에 노출되지 않습니다. aria-expanded 도 Semantics 플래그도 라이브 알림도 없이 콘텐츠 노드만 사라집니다.
  • 타이틀 바가 별도 영역이나 드래그 핸들로 노출되지 않습니다 — 역할도 라벨도 없습니다.
  • 루트의 접근성 이름이 title 이 아니라 로케일화된 일반 명사("Window"/"창") 입니다. 여러 패널을 한 화면에 놓으면 리더에서 서로 구분되지 않으므로, 구분이 필요하면 호출자가 바깥에서 라벨을 붙여야 합니다.

컴포넌트와 무관하게 적용되는 축(동작 줄이기 · 고대비 · 색 강제 모드 · 최소 터치 타겟)은 전역 접근성 축에 정리되어 있습니다.

관련 컴포넌트#

  • Dialog — 모달 대화상자
  • Drawer — 옆에서 슬라이드되는 패널
  • Card — 일반 카드 컨테이너