WindowPanel#
데스크탑 앱 창을 연상시키는 윈도우 스타일 패널입니다. 타이틀 바 · 선택적
액션 슬롯 · 최소화/최대화/닫기 컨트롤 버튼 · 콘텐츠 영역으로 구성되며,
단독으로도 쓸 수 있고 더 큰 윈도우 매니저(WindowNavigator)의
building block으로도 쓸 수 있습니다.
Live Preview#
class WindowPanelDefaultExample extends StatefulComponent {
const WindowPanelDefaultExample({super.key});
@override
State<WindowPanelDefaultExample> createState() =>
_WindowPanelDefaultExampleState();
}
class _WindowPanelDefaultExampleState
extends State<WindowPanelDefaultExample> {
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.'),
),
],
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%'},
),
);
}
}
class WindowPanelDefaultExample extends StatefulWidget {
const WindowPanelDefaultExample({super.key});
@override
State<WindowPanelDefaultExample> createState() =>
_WindowPanelDefaultExampleState();
}
class _WindowPanelDefaultExampleState
extends State<WindowPanelDefaultExample> {
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.'),
),
),
);
}
}
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%'},
),
);
}
}
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,
);
}
}
사용 시기 (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 | 타이틀 바 우측 액션 슬롯 (기본 컨트롤 버튼 앞에 배치) |
closable | bool | true | 닫기 버튼 표시 |
minimizable |
bool |
false |
최소화 버튼 표시 |
maximizable |
bool |
false |
최대화 버튼 표시 |
onClose | CoreVoidCallback? | 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.windowPanel 의 style 슬롯으로 기본값 오버라이드:
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. 타이틀 바 · 타이틀 텍스트 · 컨트롤 행 · 콘텐츠 영역은 양쪽 모두
role 도 aria-* 도 내보내지 않습니다.
키보드#
| 키 | 동작 |
|---|---|
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"/"창") 입니다. 여러 패널을 한 화면에 놓으면 리더에서 서로 구분되지 않으므로, 구분이 필요하면 호출자가 바깥에서 라벨을 붙여야 합니다.
컴포넌트와 무관하게 적용되는 축(동작 줄이기 · 고대비 · 색 강제 모드 · 최소 터치 타겟)은 전역 접근성 축에 정리되어 있습니다.