Small
컴팩트 도구와 보조 작업
배포
같은 표면과 간격 규칙.
컨테이너, 그리드, 간격, 컴포넌트 크기를 정합니다.
작업에 맞는 컨테이너를 고르세요. 컨테이너는 최대 너비까지 공간을 채우고, 그보다 넓으면 가운데에 놓입니다.
| 컨테이너 | 최대 너비 | 토큰 | 용도 |
|---|---|---|---|
| 작업 공간 | 75rem | --surf-layout-workspace | 운영 화면, 대시보드 |
| 읽기 | 52rem | --surf-layout-reading | 문서, 긴 폼 |
| 와이드 | 96rem | --surf-layout-wide | 비교 화면, 넓은 테이블 |
어느 컨테이너에서든 읽기 텍스트는 타이포그래피에서 정한 줄 길이에서 줄을 바꿉니다.
파이프라인 실행과 배포 결과를 검토하세요.
같은 크기의 카드에는 Grid 열을, 본문 옆 패널처럼 너비가 다른 영역에는 12 / 6 / 4 레이아웃 그리드를 쓰세요.
| 뷰포트 | 레이아웃 그리드 | 거터 | 같은 크기 카드 |
|---|---|---|---|
| 1100px 초과 | 12트랙 | 24px | 1–4열 |
| 761–1100px | 6트랙 | 24px | 2열 |
| 760px 이하 | 4트랙 | 16px | 1열 |
주요 콘텐츠
8 / 4 / 4트랙
보조 정보
4 / 2 / 4트랙
전체 너비 결과
12 / 6 / 4트랙, long-resource-identifiers-for-staging-deployments-also-wrap
import { Container, Grid, GridItem, Card } from '@surflane/ui'
<Container width='workspace'>
<Grid columns='layout'>
<GridItem span={8} mediumSpan={4} smallSpan={4}>
Main
</GridItem>
<GridItem span={4} mediumSpan={2} smallSpan={4}>
Details
</GridItem>
<GridItem span={12} mediumSpan={6} smallSpan={4}>
Results
</GridItem>
</Grid>
</Container>
// Equal cards: 3 columns, then 2, then 1.
// Match bleed to the card padding so card text aligns with the text around it.
<Grid columns={3} gap={4} bleed={4}>
{items.map((item) => (
<Card.Root key={item.id} padding={4}>
...
</Card.Root>
))}
</Grid>
// As many columns as the grid's own width holds, each at least 20rem.
<Grid columns='auto' min='card'>
{items.map((item) => (
<Card.Root key={item.id}>...</Card.Root>
))}
</Grid>고정 열 개수는 창이 좁아지면 단계로 줄어듭니다. 사이드바 옆이나 패널 안처럼 그리드의 공간이 창을 따르지 않는 곳에서는 columns='auto'가 그리드 자체 너비가 담는 만큼 열을 두고, 각 열은 최소 min을 지킵니다. 'tile'은 수치와 그 레이블에 맞는 10rem, 'brief'는 여러 세션 가운데 하나처럼 이름과 한두 줄을 담는 카드에 맞는 14rem, 'card'는 카드에 맞는 20rem, 'wide'는 차트나 작은 표에 맞는 30rem입니다.
bleed를 카드 패딩과 같게 두면(padding={4}에 bleed={4}) 카드 글이 주변 글과 줄을 맞춥니다. 창 가장자리나 잘리는 영역에서는 bleed를 쓰지 마세요.
넓은 테이블과 코드는 페이지를 넓히지 말고 ScrollArea 안에서 스크롤하세요.
두 요소가 얼마나 가까운 관계인지에 따라 스케일에서 간격을 고르세요.
| 관계 | 간격 | 토큰 | 예 |
|---|---|---|---|
| 한 항목의 부분 | 4–8px | --surf-space-1, --surf-space-2 | 아이콘과 라벨, 라벨과 입력 |
| 한 줄의 컨트롤 | 8px | --surf-space-2 | 액션 행의 버튼 |
| 필드와 그 액션 | 16px | --surf-space-4 | 입력과 그 아래 버튼 |
| 작업 묶음 | 24px | --surf-space-6 | 필드 묶음, 필터, 결과 |
| 섹션 | 48px | --surf-layout-section-gap | 독립된 주제나 단계 |
| 컨테이너 안쪽 | 16–32px | --surf-space-4, --surf-space-6, --surf-layout-page-inset | Popover 16px, 카드 24px, 페이지 32px |
아래 폼은 표를 따릅니다. 치수를 켜서 간격을 재 보세요.
표시할 이름을 입력하세요.
<Stack gap={4}>
<Stack gap={1}>{/* Title and description; margin: 0 */}</Stack>
<Field.Root>{/* Field.Label + Input; gap: var(--surf-space-2) */}</Field.Root>
<Inline gap={2}>{/* Action buttons */}</Inline>
</Stack>촘촘한 레이아웃은 작업 묶음 간격만 16px로 줄이고 컨트롤은 그대로 둡니다. 휴대폰에서는 페이지 인셋이 16px, 섹션 간격이 32px로 줄어듭니다. 간격은 뷰포트 높이가 아니라 스케일로 정하세요.
Small, Default, Large는 컨트롤 높이, 글자, 안쪽 간격을 바꿉니다. 화면 전체를 확대하거나 축소하지 않습니다.
| 요소 | Small | Default | Large |
|---|---|---|---|
| 버튼 / 입력 높이 | 32px | 36px | 44px |
| 필드와 메뉴 글자 | 13 / 20px | 14 / 22px | 16 / 24px |
| 버튼 글자 | 13 / 18px | 14 / 20px | 16 / 22px |
| Checkbox / Radio | 16px | 20px | 24px |
| Popover 패딩 | 12px | 16px | 24px |
| 카드 패딩 | 16px | 24px | 32px |
| 카드, 시트, 대화상자 제목 | 16 / 24px | 20 / 28px | 20 / 28px |
| Slider 손잡이 | 20px | 24px | 28px |
| 테이블 셀 세로 패딩 | 8px | 12px | 16px |
| Badge 세로 패딩 | 2px | 4px | 6px |
| Avatar | 24px | 36px | 48px |
| 트리 행 | 36px | 44px | 52px |
터치 기기와 좁은 화면에서 컨트롤은 타깃 크기 아래로 줄지 않습니다. 크기는 차트 데이터와 선 두께를 바꾸지 않습니다. 크기를 따라 바뀌는 글자는 컨트롤 글자뿐이고, 타입 역할은 모두 그대로입니다. 단, 카드, 시트, 드로어, 대화상자 제목은 Small에서 subsection 역할을 씁니다. 기본값 SizeProvider는 provider가 없을 때와 똑같이 그립니다.
컴팩트 도구와 보조 작업
같은 표면과 간격 규칙.
대부분 화면의 기본 크기
같은 표면과 간격 규칙.
주요 작업과 여유로운 입력
같은 표면과 간격 규칙.
SizeProvider는 폼, 테이블, 메뉴와 그 안에서 열리는 포털의 크기를 정합니다. 안쪽 provider나 size prop이 이를 덮어씁니다. provider는 테이블 안이 아니라 Table.Container 바깥에 두세요. density='compact'는 촘촘한 화면을 위해 안쪽 블록을 좁힙니다. 블록 사이 12px, 블록 안 8px, 카드 제목은 label 역할, 표와 차트는 compact가 되고, 터치에서도 컨트롤은 크기를 유지합니다.
import { SizeProvider, Button, Input, Select } from '@surflane/ui'
<SizeProvider size='small'>
<Input aria-label='Project name' />
<Select aria-label='Environment' options={environments} />
<Button>Apply</Button>
<Button size='large'>Start deployment</Button>
</SizeProvider>
// The short sm / md / lg spellings still work, but are deprecated.
// A numeric size on Input is the standard HTML character-width hint.밀도는 크기 옆의 두 번째 축입니다. 크기는 컨트롤이 얼마나 큰지를, 밀도는 블록이 얼마나 가까이 놓이는지를 정합니다. SizeProvider density='compact'는 모니터링 대시보드처럼 편집보다 훑어보는 영역을 촘촘하게 만듭니다. 블록 사이는 12px, 안쪽은 8px이 되고, 안의 부품은 촘촘한 형태를 씁니다.
| 부품 | compact |
|---|---|
| Dashboard 패널과 Grid 열 사이 간격, 카드 패딩, AppShell 페이지 여백과 사이드바 여백 | 12px, --surf-ui-density-gap |
| AppShell 사이드바 줄, 접기 단추, 레일의 링크 | 32px, 56px 레일 위의 정사각형 |
| 카드나 패널 안쪽 간격, FilterBar, PageHeader, Stat.Group 간격 | 8px, --surf-ui-density-inset |
| Card와 Dashboard.Panel 제목 | 레이블 역할 |
| Table, LineChart, AreaChart, BarChart | density='default'가 아니면 촘촘하게 |
| DescriptionList 줄 | 작은 줄, 32px |
| Banner, Timeline 사건 | 위아래 4px |
| 터치 화면의 컨트롤 | 제 크기, 최소 24px |
폼, 문서, 휴대폰 중심 화면은 기본 밀도로 두세요. compact는 부품 하나가 아니라 영역 전체에 정해서 이웃한 블록이 서로 맞게 하세요. 자세히 읽는 표는 density='default'로 되돌립니다.
compact는 대상을 WCAG 2.2 AA 최소치인 24px 밑으로 줄이지 않습니다. 터치 화면에서 컨트롤은 44px로 커지는 대신 제 크기를 지킵니다.
컬렉션은 세션, 호스트, 배포처럼 비슷한 항목을 Grid 안의 카드로 나란히 보여 줍니다. 읽는 사람은 같은 줄의 카드를 서로 비교하므로, 모든 카드는 같은 부분을 같은 자리에 보여야 합니다. 그리드는 주어진 공간에 카드를 맞춥니다.
| 부분 | 규칙 |
|---|---|
| 열 | Grid columns='auto' min='card'는 그리드 자체 너비에 들어오는 만큼 열을 두고, 각 열은 최소 20rem입니다. min='tile'은 수치와 그 레이블, min='brief'는 이름과 한두 줄을 담는 카드, min='wide'는 차트에 맞는 크기입니다. 고정 열 개수는 창 너비를 따르므로, 사이드바 옆이나 패널 안에서는 카드가 좁게 눌립니다. |
| 패딩과 간격 | 카드의 패딩과 그리드의 gap은 모두 밀도에서 나옵니다. 영역에 compact를 두면 카드는 서로 12px 떨어지고 안쪽 여백은 12px가 됩니다. 카드 하나하나에 패딩이나 간격을 따로 주지 마세요. |
| 헤더 | 누구인지를 보여 주는 자리입니다. categoryFor(name)이 고른 색의 Avatar, Card.Action이 들어 있는 제목, 흐린 색의 메타데이터 한 줄, StatusBadge 하나를 둡니다. 이 줄은 Inline wrap={false}로 한 줄을 지킵니다. 긴 이름은 Card.Title lines={1}로 말줄임이 되고 배지는 제자리에 남아, 모든 카드가 머리줄 높이를 지킵니다. |
| 상태 | StatusBadge 하나가 상태를 글로 말합니다. Avatar.Status의 live는 지금 작업 중인 항목에만 켭니다. 색 띠, 테두리, 색을 입힌 카드는 쓰지 마세요. 상태는 카드 위의 점으로 나타내고, 카드의 면으로 나타내지 않습니다. |
| 본문 | 마지막 메시지처럼 항목마다 다른 글은 Text lines={2}로 두어 모든 본문의 높이를 같게 합니다. 전체 글은 카드를 여는 곳에서 보입니다. |
| 푸터 | 높이가 정해진 부분만 둡니다. Meter는 한 줄을 다 쓰고, 그 아래 Stat.Group에 작은 Stat을 한 줄로 넣습니다. 카드 최소 너비에는 4개쯤 들어갑니다. 푸터가 카드 맨 아래에 놓여 같은 줄 카드의 푸터가 나란히 맞습니다. |
| 빈 값 | 부분을 빼지 말고 0이나 대시를 보이세요. 그래야 뒤의 부분이 제자리를 지킵니다. |
| 상세 | Card.Action은 Sheet를 열어 컬렉션이 화면에 남게 합니다. Dialog는 이름 바꾸기 같은 작업용입니다. 세션의 모든 턴처럼 긴 기록은 페이지를 따로 두세요. |
devbox의 Claude Code
카드 푸터를 바꾼 뒤 Playwright 테스트를 돌리는 중입니다. 640개 중 212개를 마쳤고, 아직 실패는 없습니다.
nas의 Claude Code
새 큐 설정을 적용하도록 worker 컨테이너를 재시작해도 되는지 묻습니다.
gpu-1의 opencode
패널 3개를 LineChart로 옮기고 테스트를 업데이트했습니다.
devbox의 Claude Code
인덱스 토큰이 만료되어 두 번째 샤드를 가져오다 실행이 멈췄습니다.
import {
Avatar,
Card,
Grid,
Inline,
Meter,
Sheet,
SizeProvider,
Stack,
Stat,
StatusBadge,
Text,
categoryFor,
} from '@surflane/ui'
<SizeProvider density='compact'>
<Grid columns='auto' min='card'>
{sessions.map((session) => (
<Sheet.Root key={session.id}>
<Card.Root>
<Card.Header>
<Inline justify='between' wrap={false}>
<Inline gap={3} wrap={false}>
<Avatar.Root
category={categoryFor(session.name)}
aria-hidden='true'
>
<Avatar.Fallback>{session.initials}</Avatar.Fallback>
{session.working && <Avatar.Status tone='progress' live />}
</Avatar.Root>
<Stack gap={0}>
<Card.Title lines={1}>
<Sheet.Trigger render={<Card.Action />}>
{session.name}
</Sheet.Trigger>
</Card.Title>
<Text variant='caption' color='secondary' lines={1}>
{session.agent}
</Text>
</Stack>
</Inline>
<StatusBadge tone={session.tone}>{session.state}</StatusBadge>
</Inline>
</Card.Header>
<Card.Body>
<Text lines={2}>{session.lastMessage}</Text>
</Card.Body>
<Card.Footer>
<Meter.Root value={session.context} low={60} high={85} optimum={0}>
<Meter.Label>Context</Meter.Label>
<Meter.Value />
<Meter.Track>
<Meter.Indicator />
</Meter.Track>
</Meter.Root>
<Stat.Group label='Session counts' labelHidden size='small'>
<Stat.Root>
<Stat.Label>Turns</Stat.Label>
<Stat.Value>{session.turns}</Stat.Value>
</Stat.Root>
{/* Tools, Tokens, Time: a value not yet known shows a dash. */}
</Stat.Group>
</Card.Footer>
</Card.Root>
<Sheet.Content>
{/* The whole session: counts, context, turns. */}
</Sheet.Content>
</Sheet.Root>
))}
</Grid>
</SizeProvider>두 가지를 정하면 컴포넌트가 오른쪽에서 왼쪽으로 쓰는 언어에 맞게 뒤집힙니다. 페이지나 영역의 dir='rtl'은 레이아웃을, 그것을 감싼 DirectionProvider direction='rtl'은 화살표 키와 영역 밖에 그려지는 팝업을 뒤집습니다. 스타일시트의 모든 방향은 논리 속성이라서 패딩, 정렬, 붙어 있는 부품이 읽기 방향을 따릅니다.
| 부품 | 오른쪽에서 왼쪽 |
|---|---|
| 레이아웃, 패딩, 정렬 | 뒤집힙니다 |
| Menu, Select, Popover, Tooltip 팝업 | Provider의 방향을 따릅니다 |
| Tabs, Toolbar, Menubar, Slider의 화살표 키 | 왼쪽과 오른쪽이 바뀝니다 |
| 갈매기표, 뒤로 화살표, 브레드크럼 구분자 | 뒤집힙니다 |
| 스크롤 페이드, Switch 손잡이, Tree 토글 | 뒤집힙니다 |
| 차트의 키와 범례 | 인라인 끝에 놓입니다 |
| 차트의 플롯 | 그린 대로 왼쪽에서 오른쪽으로 |
| Kbd로 적은 화살표 키 | 그대로: 물리 키를 가리킵니다 |
| 가운데 Dialog, Sheet의 side, 가로 스크롤 막대 | 물리 방향 그대로 |
이름이나 식별자처럼 방향이 섞인 글은 콘텐츠와 현지화를 보세요.