PlGallery
배치된 사진 묶음입니다. 네 가지 배치(콘택트 시트, 메이슨리, 정렬된 라이브러리, 퀼트)에 캡션과 포인터 반응, 그리고 선택적인 라이트박스가 모두 얹힙니다.
import { PlGallery } from 'plass-ui';
<PlGallery
items={[
{ src: '/harbour.jpg', alt: 'A harbour at dusk', ratio: 4 / 3 },
{ src: '/bridge.jpg', alt: 'A bridge over a river', ratio: 3 / 2 }
]}
layout="masonry"
preview
/>;import 'package:plass_ui/plass_ui.dart';
PlGallery(
items: <PlGalleryItem>[
PlGalleryItem(
image: const NetworkImage('/harbour.jpg'),
semanticLabel: 'A harbour at dusk',
ratio: 4 / 3,
),
],
layout: PlGalleryLayout.masonry,
preview: true,
);viewer는 트리 밖으로 떠오르므로 preview를 켠 갤러리 위에는 Overlay가 필요합니다. navigator가 있는 WidgetsApp과 MaterialApp이 모두 제공합니다.
네 가지 배치가 곧 이 컴포넌트입니다. 나머지(캡션, 포인터 반응, viewer)는 넷 모두에서 같고, 그중 무엇을 쓸지는 컴포넌트 넷이 아니라 prop 하나입니다.
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| items * | readonly PlGalleryItem[] | — | 그릴 순서대로의 사진들 |
| layout | 'grid' | 'masonry' | 'justified' | 'quilted' | 'grid' | 타일을 어떻게 배치할지. 네 가지 답이 아니라 네 가지 질문입니다 |
| columns | PlassResponsive<number> | { xs: 2, sm: 3, lg: 4 } | 가로로 몇 장인지, breakpoint마다. justified는 줄마다 스스로 정합니다 |
| gap | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number | string | 'md' | 타일 사이의 간격. 사다리의 한 단, 픽셀 수, 또는 CSS 길이 |
| ratio | number | string | 1 | grid에서 타일의 모양이자, 자기 ratio가 없는 item이 다른 곳에서 쓰는 값 |
| rowHeight | number | 220 | justified에서 한 줄이 지향하는 높이, quilted에서 한 칸의 높이 |
| rounded | boolean | true | 타일의 모서리를 둥글립니다 |
| caption | 'none' | 'below' | 'overlay' | 'hover' | 'none' | 타일의 title과 description이 어디에 갈지. hover는 포인터와 함께 오는 overlay입니다 |
| hover | 'none' | 'lift' | 'dim' | 'zoom' | 'lift' | 포인터 아래에서 타일이 하는 일. zoom은 그대로 있는 프레임 안에서 사진만 움직입니다 |
| preview | boolean | false | 타일을 누르면 사진을 원본 크기로 엽니다. viewer는 필요할 때만 받아 옵니다 |
| onItemSelect | (item: PlGalleryItem, index: number) => void | — | 타일을 골랐을 때. viewer가 있든 없든 |
| label | string | 'Gallery' | 목록의 접근성 이름 |
| itemLabel | (index: number, total: number) => string | (i, n) => `${i} of ${n}` | 타일과 viewer의 카운터가 세트 안의 위치를 말하는 방식 |
| empty | ReactNode | — | items가 비었을 때 그릴 것. 기본은 아무것도 그리지 않습니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 타입 스케일과 모서리 반경 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. focus ring과 placeholder에 닿습니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| items * | List<PlGalleryItem> | — | 그릴 순서대로의 사진들 |
| layout | PlGalleryLayout | PlGalleryLayout.grid | 타일을 어떻게 배치할지. 네 가지 답이 아니라 네 가지 질문입니다 |
| columns | PlassResponsive<int> | PlassResponsive(2, sm: 3, lg: 4) | 가로로 몇 장인지, breakpoint마다. justified는 줄마다 스스로 정합니다 |
| gap | double? | the size ladder's step | 타일 사이의 간격. 사다리의 한 단, 픽셀 수, 또는 CSS 길이 |
| ratio | double | 1 | grid에서 타일의 모양이자, 자기 ratio가 없는 item이 다른 곳에서 쓰는 값 |
| rowHeight | double | 220 | justified에서 한 줄이 지향하는 높이, quilted에서 한 칸의 높이 |
| rounded | bool | true | 타일의 모서리를 둥글립니다 |
| caption | PlGalleryCaption | PlGalleryCaption.none | 타일의 title과 description이 어디에 갈지. hover는 포인터와 함께 오는 overlay입니다 |
| hover | PlGalleryHover | PlGalleryHover.lift | 포인터 아래에서 타일이 하는 일. zoom은 그대로 있는 프레임 안에서 사진만 움직입니다 |
| preview | bool | false | 타일을 누르면 사진을 원본 크기로 엽니다. viewer는 필요할 때만 받아 옵니다 |
| onItemSelected | void Function(PlGalleryItem, int)? | — | 타일을 골랐을 때. viewer가 있든 없든 |
| semanticLabel | String? | 'Gallery' | 목록의 접근성 이름 |
| itemLabel | String Function(int, int)? | (i, n) => `${i} of ${n}` | 타일과 viewer의 카운터가 세트 안의 위치를 말하는 방식 |
| empty | Widget? | — | items가 비었을 때 그릴 것. 기본은 아무것도 그리지 않습니다 |
| size공통 | PlassSize | PlassSize.md | 타입 스케일과 모서리 반경 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. focus ring과 placeholder에 닿습니다 |
PlGalleryItem
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| src * | string | — | 사진의 주소 |
| alt * | string | — | 사진이 말하는 것. PlImage가 요구하는 이유와 같습니다 |
| id | string | — | 안정적인 식별자. 기본은 src입니다 |
| title | ReactNode | — | 캡션의 첫 줄 |
| description | ReactNode | — | 두 번째 줄. 한 단 작고 muted입니다 |
| full | string | — | viewer가 쓸 더 큰 파일. 없으면 src로 떨어집니다 |
| ratio | number | string | — | 사진 자신의 비율. masonry와 justified가 이것으로, 아무것도 불러오기 전에 배치됩니다 |
| cols | number | 1 | quilted에서 차지하는 열 수 |
| rows | number | 1 | quilted에서 차지하는 행 수 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| image * | ImageProvider<Object> | — | 사진 |
| semanticLabel * | String | — | 사진이 말하는 것. PlImage가 요구하는 이유와 같습니다 |
| id | String? | — | 안정적인 식별자. 기본은 image provider 자신입니다 |
| title | String? | — | 캡션의 첫 줄 |
| description | String? | — | 두 번째 줄. 한 단 작고 muted입니다 |
| full | ImageProvider<Object>? | — | viewer가 쓸 더 큰 파일. 없으면 src로 떨어집니다 |
| ratio | double? | — | 사진 자신의 비율. masonry와 justified가 이것으로, 아무것도 불러오기 전에 배치됩니다 |
| cols | int | 1 | quilted에서 차지하는 열 수 |
| rows | int | 1 | quilted에서 차지하는 행 수 |
native <ul> 속성은 그대로 전달됩니다. children은 사진이 items이기 때문에, onSelect는 이 컴포넌트의 것이 onItemSelect이고 event가 아니라 item을 넘기기 때문에 제외됩니다.
className은 목록에 붙습니다. classNames는 그 안의 다섯 부분(item, image, caption, title, description)에 닿습니다.
라이브러리 전체에서 공유 축이 뜻하는 바는 prop 규약에 있습니다.
Examples
layout
grid는 콘택트 시트입니다. 파일이 어떤 모양이든 모든 타일이 같은 모양이 됩니다. masonry는 사진마다 자기 비율을 지키며 열을 쌓습니다. justified는 비율을 지키면서 동시에 모든 줄을 가장자리까지 채웁니다. 사진 라이브러리가 쓰는 배치이고, 아무것도 잘리지 않으면서 남는 공간도 없는 유일한 배치입니다. quilted는 타일이 한 칸 이상을 차지할 수 있는 격자입니다.
메이슨리는 아래로 내려가기 전에 옆으로 갑니다. CSS columns는 첫 열을 위에서 아래까지 채우고 나서 둘째 열을 시작하므로, 1부터 12까지 번호가 매겨진 묶음은 왼쪽 가장자리를 따라 읽히고 처음 만나는 세 장이 세로로 포개집니다. 이렇게 나누면 첫 줄이 1, 2, 3이고, 그것이 주어진 순서입니다.
ratio
모든 배치는 측정한 값이 아니라 item 자신의 ratio로 이루어집니다. 사진 마흔 장의 벽이 첫 프레임부터 제자리에 있고 파일이 도착하는 동안 다시 흐르지 않는 이유입니다. ratio가 없는 묶음은 갤러리의 ratio로 떨어지고, 메이슨리의 옷을 입은 정사각형 격자가 됩니다.
{ src: '/dunes.jpg', alt: 'Dunes at first light', ratio: 2 }
{ src: '/terrace.jpg', alt: 'A stepped terrace', ratio: '2 / 3' }숫자든 CSS가 쓰는 방식이든 됩니다. 2도 '2 / 3'도 동작하는데, 비율은 원래 그렇게 쓰이고 이 라이브러리는 호출자에게 번역을 시키지 않기 때문입니다.
double이고, 너비 나누기 높이입니다. 문자열 형태는 없습니다. Dart에는 맞춰야 할 CSS가 없기 때문입니다.
여기서는 배치 둘이 측정을 하고 React에서는 하나도 하지 않습니다. CSS는 justified를 flex-grow로, 퀼트를 grid-auto-flow: dense로 처리합니다. Flutter에는 그런 것이 없으므로 그 둘은 LayoutBuilder 안에서 스스로 packing합니다. 결과 배치는 같고, 다른 것은 누가 계산했는가입니다.
caption
below는 두 줄을 사진 아래에 두고, overlay는 옅은 사진에서도 글자가 살아남을 만큼 어두운 wash 위에 사진 밑단을 가로질러 씁니다. hover는 포인터와 함께 오는 overlay입니다.
title도 description도 없는 타일은 caption이 무엇이든 캡션을 그리지 않습니다. 한 줄에 캡션 하나와 빈자리 셋이 있는 편보다 아예 없는 편이 낫습니다.
hover
lift와 dim은 깊이와 색이고, 이 라이브러리의 다른 모든 것이 포인터에 답하는 방식입니다. zoom만 크기를 바꾸는데, 디자인 언어가 명시한 예외입니다. 움직이는 것은 그대로 있는 프레임 안의 사진이고, 그 위에는 다시 그려질 글자가 없습니다.
quilted
타일은 격자에서 cols개의 열과 rows개의 행을 차지합니다. 흐름은 dense입니다. 줄에 남은 자리에 비해 너무 넓은 타일은 모두를 아래로 밀지 않고 자기가 들어갈 다음 줄로 내려가며, 뒤의 좁은 타일이 그 구멍을 채웁니다.
격자보다 넓은 span은 거절하지 않고 자릅니다. cols: 99라고 쓴 사람이 뜻한 바가 그것입니다.
preview
사진을 원본 크기로 열고, 나머지는 화살표 키 하나 거리에 둡니다. 캐러셀이 아닙니다. 캐러셀은 누군가에게 순서대로 보여 주는 묶음이고 이것은 다음으로 가는 길이 있는 사진 한 장입니다. 그래서 autoplay도, 순환도 없고, 화살표는 이미 본 사진으로 돌아가는 대신 양 끝에서 멈춥니다.
full은 타일이 썸네일일 때 쓸 더 큰 파일입니다. 사진마다 크기가 하나뿐인 묶음은 아무것도 적지 않아도 됩니다.
{ src: '/thumb/harbour.jpg', full: '/full/harbour.jpg', alt: 'A harbour at dusk' }viewer는 React.lazy 뒤에 있으므로, 아무도 열지 않은 라이트박스에 썸네일 벽이 비용을 치르지 않습니다. PlImage가 같은 prop으로 하는 것과 같은 거래입니다.
Accessibility
- 이름이 붙은 진짜
role="list"이고, 사진 하나당role="listitem"하나입니다. 메이슨리의 lane은<ul>과<li>사이의<div>가 아니라 자기 목록을 담은 list item입니다. 그 사이의<div>는 스크린 리더가 아무것도 없는 목록으로 읽는 마크업입니다. - 타일은 눌렀을 때 무슨 일이 일어날 때만 button입니다. 이름은 사진 자신의 말에 세트 안의 위치를 더한 것으로, "A harbour at dusk — 1 of 6"과 같은 형태입니다. 그래서 썸네일 벽을 tab으로 지나가는 사람은 전체 몇 중 몇 번째에 있는지 듣습니다.
itemLabel은 그 문장을 다른 언어로 쓰는 방법이고, 어순이 다르기 때문에 슬롯이 든 문자열이 아니라 콜백입니다.- viewer의 화살표 키는 버튼이 아니라 시트에 묶여 있습니다. focus는 읽는 사람이 마지막으로 둔 자리에 있고, 한 곳에서만 동작하는 키는 나머지 모든 곳에서 고장 난 키로 보입니다.
- viewer의 카운터는 live region입니다. 화살표 키가 어디에 닿았는지, 그 사진을 볼 수 없는 사람에게도 알려 줍니다.