본문으로 건너뛰기

PlStack

얼굴이든 카드든 썸네일이든, 넘긴 것을 겹쳐 쌓습니다. 상자가 그린 것을 정확히 재므로, 더미가 문장 안이나 표 칸 안에 앉아도 실제 크기 그대로입니다.

React
tsx
import { PlAvatar, PlStack } from 'plass-ui';

<PlStack ring max={4} total={11} overflow={(hidden) => <PlAvatar initials={`+${hidden}`} />}>
  <PlAvatar name="Ada Lovelace" src="/ada.jpg" />
  <PlAvatar name="Grace Hopper" />
</PlStack>;
dart
import 'package:plass_ui/plass_ui.dart';

PlStack(
  max: 4,
  total: 11,
  ring: BorderRadius.circular(999),
  overflow: (int hidden) => PlAvatar(initials: '+$hidden'),
  children: const <Widget>[
    PlAvatar(name: 'Ada Lovelace'),
    PlAvatar(name: 'Grace Hopper'),
  ],
);

Props

Prop타입기본값설명
directionPlassResponsive<'horizontal' | 'vertical' | 'diagonal'>'horizontal'더미가 자라는 방향. diagonal은 진짜 45°가 아니라 부채꼴입니다 — 가로 전진량은 항목 너비에서 overlap을 뺀 값인데, 임의의 자식을 받는 컴포넌트는 그 너비를 모릅니다
overlapnumber | stringa fraction of size흐르는 축에서 각 항목이 앞 항목 아래로 들어가는 거리 — 픽셀 숫자 또는 CSS 길이
dropnumber | stringwhatever overlap resolved todiagonal에서만 쓰이는, 흐르지 않는 축의 낙차
size공통'xs' | 'sm' | 'md' | 'lg' | 'xl''md'기본 overlap이 어느 칸에서 나오는지. 그 외에는 아무것도 정하지 않습니다 — 더미는 자기 표면도 글자도 없습니다
maxnumber그리는 개수. 빼면 전부 그립니다
totalnumber앞의 몇 개만 넘겼을 때의 전체 개수. 없으면 자식 수로 셉니다
overflow(hidden: number) => ReactNode넘친 개수를 받아 맨 뒤 항목을 만듭니다. 노드가 아니라 함수인 이유는 그 숫자가 곧 항목이기 때문입니다
front'first' | 'last''last'어느 쪽 끝이 맨 위인지. last는 DOM이 스스로 하는 답이고 얼굴 줄이 원하는 것, first는 카드 덱입니다
scaleStepnumber1뒤로 갈수록 곱해지는 배율. 독립 scale 속성에 실리므로 caller가 건 transform이 살아남습니다
opacityStepnumber1투명도에 대한 같은 값
ringbooleanfalse각 항목 둘레에 페이지 표면색 실선을 그립니다. 넘긴 요소 자신에 걸리므로 그 요소의 모양을 따릅니다
childrenReactNode더미에 쌓이는 것들
Prop타입기본값설명
children * List<Widget>더미에 쌓이는 것들
directionPlassResponsive<PlStackDirection>PlStackDirection.horizontal더미가 자라는 방향. diagonal은 진짜 45°가 아니라 부채꼴입니다 — 가로 전진량은 항목 너비에서 overlap을 뺀 값인데, 임의의 자식을 받는 컴포넌트는 그 너비를 모릅니다
overlapdouble?a fraction of size흐르는 축에서 각 항목이 앞 항목 아래로 들어가는 거리(논리 픽셀)
dropdouble?whatever overlap resolved todiagonal에서만 쓰이는, 흐르지 않는 축의 낙차
size공통PlassSizePlassSize.md기본 overlap이 어느 칸에서 나오는지. 그 외에는 아무것도 정하지 않습니다 — 더미는 자기 표면도 글자도 없습니다
maxint?그리는 개수. 빼면 전부 그립니다
totalint?앞의 몇 개만 넘겼을 때의 전체 개수. 없으면 자식 수로 셉니다
overflowWidget Function(int hidden)?넘친 개수를 받아 맨 뒤 항목을 만듭니다. widget이 아니라 builder인 이유는 그 숫자가 곧 항목이기 때문입니다
frontPlStackFrontPlStackFront.last어느 쪽 끝이 맨 위인지. last는 DOM이 스스로 하는 답이고 얼굴 줄이 원하는 것, first는 카드 덱입니다
scaleStepdouble1뒤로 갈수록 곱해지는 배율. 독립 scale 속성에 실리므로 caller가 건 transform이 살아남습니다
opacityStepdouble1투명도에 대한 같은 값
ringBorderRadius?null각 항목 둘레에 그리는 페이지 표면색 실선의 모서리. bool이 아니라 반지름인 것이 React와 갈리는 한 곳입니다 — 거기서는 CSS가 요소 자신의 border-radius를 공짜로 주지만, 여기서는 자식의 모양을 읽을 방법이 없습니다
semanticLabelString?이 더미가 무엇의 집합인지. 다른 것이 그것을 말하고 있지 않을 때 이름을 주세요

네이티브 <div> 속성은 aria-label을 포함해 그대로 통과합니다. 얼굴이 늘어선 줄은 어떤 집합의 그림이고, 그것이 무엇의 집합인지는 보통 옆에 있는 문장입니다.

semanticLabel이 더미에 이름을 주고, 항목들은 그 아래에서 자기 semantics 노드를 그대로 유지합니다. 그래서 이름이 붙은 더미는 이름으로 먼저 읽히고 그다음에 안에 있는 것들로 읽힙니다.

size는 기본 overlap이 사다리의 어느 칸에서 나오는지만 고르고 그 외에는 아무것도 정하지 않습니다. 더미는 자기 표면도 없고 안에 글자도 없으므로 정할 높이도 칠할 잉크도 없습니다. 항목들은 원래 그것들이던 그대로입니다.

겹침을 만드는 방법

이 컴포넌트 전체가 딛고 선 결정이고, 틀리기 쉬운 바로 그 지점입니다.

겹쳐 쌓기는 각 항목을 앞의 것 위로 translate하는 방식으로 만들고 싶어집니다. 그렇게 하면 더미는 항목 하나 너비로 배치됩니다. 자기 상자 바깥에 그려지고, 페이지에서 그 뒤에 오는 모든 요소가 독자가 결코 보지 못하는 크기를 기준으로 놓입니다. 문단 안에도, 표 칸 안에도, 라벨 옆 flex 줄 안에도 무언가를 밀어내지 않고는 들어갈 수 없습니다.

그래서 겹침은 진짜 레이아웃입니다. React에서는 negative margin이고, Flutter에서는 자체 render object입니다. 그쪽은 EdgeInsetsFlex.spacing도 음수가 아님을 assert하기 때문입니다. 32px 항목 다섯 개에 overlap 10px이면 정확히 이렇게 잽니다.

direction상자
horizontal120 × 32
vertical32 × 120
diagonal120 × 72

마지막 줄이 두 번 읽을 값어치가 있습니다. flow는 자기가 흐르는 축에서만 겹칩니다. 그래서 diagonalhorizontal처럼 가로로 흐르고, 세로 방향은 항목마다 한 칸씩 따로 내려갑니다. 줄 안에서 고정된 오프셋 하나를 주면 모든 항목이 같은 높이에 놓이고 부채꼴은 그냥 줄이 됩니다.

그리고 이것이 diagonal이 진짜 45°가 아니라 부채꼴인 이유이기도 합니다. 가로 전진량은 항목 너비 − overlap인데, 임의의 자식을 받는 컴포넌트는 그 너비를 알지 못합니다. drop이 세로 걸음이고, 둘은 의도적으로 독립입니다.

React

direction은 반응형입니다. 그래서 더미가 노트북에서는 가로로, 폰에서는 세로로 갈 수 있습니다. 서버는 xs 항목을 렌더링하고 브라우저가 hydration에서 고칩니다.build에서 창 너비를 기준으로 풀리므로 첫 프레임부터 정확합니다. 브레이크포인트 참고.

Examples

max, total, overflow

max는 몇 개를 그릴지, total은 실제로 몇 개인지입니다. 앞의 몇 개만 넘겼을 때를 위한 것입니다. overflow는 그 차이를 받아 마지막 항목을 그립니다.

노드가 아니라 함수인 것이 핵심입니다. 그 숫자가 곧 항목이기 때문입니다. 노드였다면 스스로 알아낼 방법이 없는 개수를 받아야 하고, 목록이 바뀔 때마다 틀리게 됩니다.

React

front, scaleStep, opacityStep

front는 목록의 어느 쪽 끝이 맨 위인지 말합니다. last는 DOM이 스스로 하는 답이고 얼굴이 늘어선 줄이 원하는 것입니다. 가장 최근에 온 사람이 앞에 있습니다. first는 카드 덱입니다. 맨 위 카드가 먼저 읽는 카드입니다.

scaleStepopacityStep앞에 있는 쪽에서부터 멀어지며 곱해집니다. 그래서 맨 앞 항목은 언제나 온전한 크기이고, front를 뒤집었다고 이쪽까지 뒤집을 필요가 없습니다. 그리는 시점에 적용되므로 뒤로 물러난 항목도 원래 차지하던 자리를 그대로 차지하고, 걸음 간격이 고르게 유지됩니다.

React

ring

비슷한 톤의 두 도형을 겹쳐 놓으면 그 사이에 경계가 전혀 없어서 더미가 하나의 뭉개진 형태로 읽힙니다. 이 실선은 페이지 자신의 표면색이라, 무언가를 둘러싼 선이 아니라 가까운 항목이 오려내진 구멍으로 읽힙니다. 반투명한 선은 도움이 되지 않습니다. 그 뒤에 있는 것이 다른 항목이기 때문입니다.

넘긴 요소 자신에 걸리므로 그 요소의 모양을 따릅니다. avatar를 사각형인 무언가로 감싸면 실선도 사각형이 됩니다. 임의의 자식을 받는 컴포넌트가 그보다 더 잘 알 방법은 없습니다.

bool이 아니라 BorderRadius를 받고, 이것이 React 빌드와 갈리는 유일한 지점입니다. 저쪽에서 ring은 box shadow이고 CSS가 요소 자신의 border-radius를 공짜로 줍니다. 여기서는 자식의 모양을 읽을 수 있는 것이 없으므로 모양을 말해 주어야 합니다.

PlAvatarGroup에서 옮겨 오기

PlAvatarGroup은 자식의 종류 하나가 안에 새겨진 이 컴포넌트였고, 이제 없습니다. 얼굴이 늘어선 줄은 더미의 한 가지 배치이지 별도의 컴포넌트가 아닙니다.

옮겨지는 것들:

PlAvatarGroupPlStack
<PlAvatarGroup><PlStack>
max, total, overlap그대로
알아서 그려 주던 +n avataroverflow={(n) => <PlAvatar initials={\+${n}`} />}`
모든 avatar에 한 번에 걸던 size, color스택을 감싸는 PlassProvider, 또는 avatar마다의 prop
한 번에 걸던 shape, variant, elevationavatar마다의 prop
언제나 그려지던 ringring

진짜로 잃는 것은 그룹 context이고, 그것은 지킬 수 없었습니다. 임의의 자식을 받는 더미는 그중 하나가 avatar라는 것을 알 방법이 없습니다. sizecolor는 애플리케이션이 이미 한 번에 정하는 축이니 스택을 PlassProvider(Flutter에서는 PlassTheme)로 감싸세요. 나머지 셋은 애초에 애플리케이션 전체의 축이었던 적이 없고, avatar에 있는 것이 맞습니다.

얻는 것은 그룹이 할 수 없던 전부입니다. 어떤 자식이든, 세 가지 방향, 직접 말할 수 있는 쌓기 순서, 깊이, 그리고 직접 그리는 넘침 항목.

Accessibility

  • 스택은 role도 label도 붙이지 않습니다. 이미 자기가 무엇인지 알리는 내용을 감싼 <div>입니다. 얼굴 줄이 옆의 어떤 것도 이름 붙여 주지 않는 집합을 대신하고 있다면 aria-label을 주세요.
  • 항목들은 자기 요소, 자기 semantics, 자기 focus 순서를 그대로 유지합니다. 복제되는 것도 대체되는 것도 없습니다.
  • 쌓기 순서는 z-index이므로 무엇이 위에 그려지는지와 포인터가 어디에 닿는지를 바꿉니다. 읽는 순서는 바꾸지 않습니다. 작성한 순서 그대로이고, 그것이 원하는 바입니다. 스크린 리더는 어느 얼굴이 앞에 있든 집합을 주어진 순서대로 읽어야 합니다.
  • semanticLabel이 더미에 이름을 주고, 항목들은 그 아래에서 자기 노드를 유지합니다.
  • hit test는 앞에서 뒤로 진행하며, 이는 그리는 순서의 반대입니다. 어느 지점에서 독자가 보고 있는 항목이 손가락이 닿는 항목입니다.
  • 쌓기 순서는 그리기와 hit test만 바꿉니다. semantics 순서는 작성한 순서 그대로입니다.

React 빌드와 다른 점

ReactFlutter
boolringBorderRadius?ringCSS는 ring에 요소 자신의 border-radius를 공짜로 줍니다. 여기서는 자식의 모양을 읽을 수 있는 것이 없으므로 말해 주어야 합니다.
negative margin자체 render objectEdgeInsetsFlex.spacing도 음수가 아님을 assert하므로, 자식 크기를 알 수 있는 유일한 자리는 우리 자신의 레이아웃입니다. 어느 쪽이든 같은 상자를 보고합니다.
overflow: (n) => ReactNodeoverflow: Widget Function(int)?같은 함수를, 각 프레임워크의 철자로.
길이나 숫자인 overlap, drop논리 픽셀 double?여기에는 쓸 CSS 길이가 없습니다.
aria-labelsemanticLabelFlutter의 이름입니다.
className, style통과시킬 class 목록도 style 속성도 없습니다.

Released under the MIT License