PlToolbar
컨트롤이 늘어선 바입니다. 애플리케이션 헤더, 페이지의 액션 줄, 에디터 아래를 가로지르는 띠에 씁니다. 슬롯 셋과 한 줄이 전부입니다.
import { PlButton, PlToolbar, PlTypography } from 'plass-ui';
<PlToolbar
render={<header />}
start={<PlTypography level="h6">Reports</PlTypography>}
end={<PlButton>New</PlButton>}
/>;import 'package:plass_ui/plass_ui.dart';
PlToolbar(
start: const <Widget>[PlTypography('Reports', level: PlTypographyLevel.h6)],
end: <Widget>[PlButton(onPressed: create, child: const Text('New'))],
);Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'glass' | 'ghost' | 'glass' | 바의 재질. 색이 들어가지 않습니다 — 툴바는 남의 컨트롤을 담습니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 바의 여백과 반경. 높이는 안에 든 컨트롤이 정합니다 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다. 타입 스케일은 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 드롭 섀도 깊이. 고정되어도 0입니다 — 헤더 아래 그림자는 스크롤된 뒤에야 참이 됩니다 |
| position | 'static' | 'sticky' | 'fixed' | 'static' | 페이지 스크롤 안에서 바가 놓이는 방식. sticky는 자기 자리를 차지하고, fixed는 흐름에서 빠집니다 |
| side | 'top' | 'bottom' | 'top' | position이 static이 아닐 때 붙잡히는 가장자리 |
| divider | boolean | false | 내용을 향한 가장자리에 얇은 선을 긋습니다 |
| start | ReactNode | — | 바의 시작에 고정되는 것 — 로고, 제목, 뒤로 가기 |
| end | ReactNode | — | 끝에 고정되는 것 — 액션들 |
| render | useRender.RenderProp | — | div가 아닌 다른 요소로 렌더링합니다 — header, nav |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| child | Widget? | — | 가운데. start와 end가 남긴 너비를 차지합니다 |
| start | List<Widget>? | — | 바의 시작에 고정되는 것 — 로고, 제목, 뒤로 가기. Dart에는 fragment가 없으니 목록을 받고 간격도 줍니다 |
| end | List<Widget>? | — | 끝에 고정되는 것 — 액션들 |
| divider | bool | false | 내용을 향한 가장자리에 얇은 선을 긋습니다 |
| side | PlassSide | PlassSide.top | 바가 향한 쪽. 그것에 달린 것은 하나뿐입니다 — divider를 어느 가장자리에 긋는지 |
| rounded | bool | true | 바가 모서리를 가진 시트인지. 화면 가장자리에 붙잡아 둘 때 끕니다 — 맞닿은 둥근 모서리는 뒤에 아무것도 없는 틈입니다 |
| variant공통 | PlassVariant | PlassVariant.glass | 바의 재질. 색이 들어가지 않습니다 — 툴바는 남의 컨트롤을 담습니다 |
| size공통 | PlassSize | PlassSize.md | 바의 여백과 반경. 높이는 안에 든 컨트롤이 정합니다 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | PlassDensity | PlassDensity.standard | 여백만 바꿉니다. 타입 스케일은 그대로 |
| elevation공통 | int | 0 | 드롭 섀도 깊이. 고정되어도 0입니다 — 헤더 아래 그림자는 스크롤된 뒤에야 참이 됩니다 |
| semanticLabel | String? | — | 바 자신에게 이름이 필요할 때 스크린 리더가 읽을 이름 |
나머지 <div> 속성은 모두 전달되고, render로 요소를 바꿉니다.
라이브러리 전체에서 공유하는 축이 무엇을 뜻하는지는 prop 규약에 있습니다.
높이
툴바는 안에 든 컨트롤에 자기 여백을 더한 만큼 높고, 그 여백은 다른 모든 표면이 쓰는 size / density 쌍입니다. 그래서 density="compact"가 같은 말을 하는 두 번째 prop 없이 촘촘한 바를 주고, 그 밑에서 타입 스케일은 움직이지 않습니다.
import { PlButton, PlToolbar, PlTypography } from 'plass-ui';
export default function ToolbarDensity() {
return (
<div className="flex w-full max-w-lg flex-col gap-3">
{(['default', 'compact'] as const).map((density) => (
<PlToolbar
key={density}
density={density}
start={<PlTypography level="caption">density: {density}</PlTypography>}
end={
<PlButton size="sm" density={density}>
Save
</PlButton>
}
/>
))}
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ToolbarDensity extends StatelessWidget {
const ToolbarDensity({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: <Widget>[
for (final PlassDensity density in PlassDensity.values)
PlToolbar(
density: density,
start: <Widget>[
PlTypography('density: ${density.name}', level: PlTypographyLevel.caption),
],
end: <Widget>[
PlButton(
size: PlassSize.sm,
density: density,
onPressed: () {},
child: const Text('Save'),
),
],
),
],
);
}
}toolbar role 없음
의도된 것입니다. role="toolbar"는(그리고 그 뒤에 있는 시맨틱은) 키보드 동작에 대한 약속입니다. 바 전체에 탭 정지 하나, 그 안의 컨트롤 사이는 방향키. 그것을 구현하지 않은 채 선언한 바는 아무것도 선언하지 않은 바보다 키보드 독자에게 더 나쁩니다.
진짜로 roving focus를 갖는 선택 묶음이 원하는 것은 PlSegmentedButton이고, 그것은 실제로 그렇습니다.
페이지 헤더가 원하는 것은 올바른 요소입니다. render={<header />}.
Examples
세 개의 슬롯
start와 end는 양 끝에 고정되고 가운데가 남는 자리를 차지합니다. 모든 툴바가 늘 취해 온 배치이므로, 호출하는 쪽과 그들이 기억해야 할 여백 채우개에 맡기는 대신 여기서 배치합니다. 가운데는 비어 있어도 자기 너비를 지킵니다. 그러지 않으면 양 끝이 바 한가운데로 모여 버립니다.
import { PlButton, PlSegment, PlSegmentedButton, PlToolbar, PlTypography } from 'plass-ui';
export default function ToolbarSlots() {
return (
<PlToolbar
className="w-full max-w-lg"
divider
start={<PlTypography level="h6">Invoices</PlTypography>}
end={<PlButton size="sm">Export</PlButton>}
>
<PlSegmentedButton size="sm" defaultValue="all">
<PlSegment value="all">All</PlSegment>
<PlSegment value="open">Open</PlSegment>
<PlSegment value="paid">Paid</PlSegment>
</PlSegmentedButton>
</PlToolbar>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ToolbarSlots extends StatefulWidget {
const ToolbarSlots({super.key});
@override
State<ToolbarSlots> createState() => _ToolbarSlotsState();
}
class _ToolbarSlotsState extends State<ToolbarSlots> {
String _filter = 'all';
@override
Widget build(BuildContext context) {
return PlToolbar(
divider: true,
start: const <Widget>[PlTypography('Invoices', level: PlTypographyLevel.h6)],
end: <Widget>[PlButton(size: PlassSize.sm, onPressed: () {}, child: const Text('Export'))],
child: PlSegmentedButton<String>(
size: PlassSize.sm,
semanticLabel: 'Filter',
value: _filter,
onChanged: (String next) => setState(() => _filter = next),
segments: const <PlSegment<String>>[
PlSegment<String>(value: 'all', label: Text('All')),
PlSegment<String>(value: 'open', label: Text('Open')),
PlSegment<String>(value: 'paid', label: Text('Paid')),
],
),
);
}
}variant
세 재질을 컨테이너의 방식으로 씁니다. 바에는 색이 들어가지 않습니다. PlBox와 같습니다. 툴바는 남의 컨트롤을 담고, 그 컨트롤들은 자기 색을 가지고 옵니다.
import { PlButton, PlToolbar, PlTypography } from 'plass-ui';
export default function ToolbarVariants() {
return (
<div className="flex w-full max-w-lg flex-col gap-3">
{(['glass', 'solid', 'ghost'] as const).map((variant) => (
<PlToolbar
key={variant}
variant={variant}
start={<PlTypography level="caption">{variant}</PlTypography>}
end={
<PlButton size="sm" variant="ghost">
Action
</PlButton>
}
/>
))}
</div>
);
}import 'package:flutter/widgets.dart';
import 'package:plass_ui/plass_ui.dart';
class ToolbarVariants extends StatelessWidget {
const ToolbarVariants({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: <Widget>[
for (final PlassVariant variant in PlassVariant.values)
PlToolbar(
variant: variant,
start: <Widget>[PlTypography(variant.name, level: PlTypographyLevel.caption)],
end: <Widget>[
PlButton(
size: PlassSize.sm,
variant: PlassVariant.ghost,
onPressed: () {},
child: const Text('Action'),
),
],
),
],
);
}
}가장자리에 붙잡아 둘 때
static은 바를 흐름 안에 둡니다. sticky는 페이지가 거기까지 스크롤되면 가장자리에 붙잡아 두고, 그러면서도 자기 자리를 계속 차지합니다. 그래서 아래쪽에 여백을 따로 줄 필요가 없습니다. fixed는 흐름에서 아예 빼내고, 그러면 페이지가 자기 여백을 가져야 합니다. 그러지 않으면 첫 화면이 바 뒤에 놓입니다.
고정된 바는 모서리를 잃습니다. 화면 가장자리에 맞닿은 둥근 모서리는 뒤에 아무것도 없는 틈입니다.
여기에는 position이 없습니다. PlFloatingBottomNavigation에 없는 것과 같은 이유입니다. fixed 요소는 무언가를 가로질러야 하고, Flutter 위젯은 화면이 놓아 준 자리에 정확히 놓입니다. 자리를 지켜야 하는 바는 화면 자신의 레이아웃에 속합니다: Positioned를 둔 Stack이거나, 내용이 그 아래로 스크롤되는 Column의 맨 위.
남는 것은 눈에 보이는 결과 하나, rounded입니다. 레이아웃 안에 앉은 바에서는 켜고, 가장자리에 붙잡아 둔 바에서는 끕니다. 화면 가장자리에 맞닿은 둥근 모서리는 뒤에 아무것도 없는 틈이기 때문입니다.
그다음 side가 정하는 것은 하나뿐입니다. divider가 얇은 선을 어느 가장자리에 긋는지, top 바에서는 아래, bottom 바에서는 위에.
elevation은 고정되어도 0으로 남는데, 그것도 의도된 것입니다. 헤더 아래의 그림자는 "이 밑에 내용이 있다"고 말하는 방식이고, 그 말이 참이 되는 것은 페이지가 스크롤된 뒤부터입니다. 그때 직접 올리거나, 평평하게 두고 divider를 켜세요.
Accessibility
- 바는 자기 role을 선언하지 않습니다.
- 안의 컨트롤들은 읽히는 순서 그대로의 평범한 컨트롤이고 각자 focus stop을 가집니다. roving focus를 약속하지 않은 바가 키보드 독자에게 빚진 것이 그것입니다.
- 바가 무엇인지는 렌더링하는 요소가 정합니다.
render={<header />}와render={<nav />}가 가장 자주 나오는 둘입니다. 페이지의 헤더는<header>여야 합니다.
semanticLabel은 바 자신에게 이름이 필요할 때 그 이름을 줍니다. 안의 컨트롤들은 자기 노드를 그대로 유지하므로, 그 이름은 바의 것이지 바와 그 안의 전부를 한 덩어리로 읽은 것이 아닙니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
position | — | fixed 요소는 무언가를 가로질러야 합니다. Flutter 위젯은 화면이 놓아 준 자리에 정확히 놓이고, 자리를 지켜야 하는 바는 화면 자신의 레이아웃에 속합니다. |
모서리가 position을 따름 | rounded | 같은 판단을 곧바로 말합니다. 흐름 안에서는 켜고, 가장자리에서는 끕니다. |
side가 붙는 가장자리와 선의 가장자리를 정함 | side가 선의 가장자리를 정함 | 그 외에 정할 것이 남지 않습니다. |
render | 바를 짓는 자리의 요소 | 바꿔 끼울 요소가 없습니다. 바에 이름을 주는 것은 semanticLabel입니다. |
노드 하나인 start, end | List<Widget> | Dart에는 fragment가 없으니, 슬롯이 어차피 담게 될 목록을 그대로 받고 간격도 대신 줍니다. |
children | child | 슬롯 하나이고, Dart는 그것을 child라고 씁니다. |
className, style | — | 전달할 클래스 목록도 style 속성도 없습니다. |