Skip to content

Commit a3780cd

Browse files
te6-inclaude
andcommitted
feat: breakpoint & responsive styling support (#1326)
* chore(docs): add chromatic viewport modes baseline Add VIEWPORT_MODES and viewport options to storybook for chromatic visual snapshot baselines across breakpoints. Hardcoded values will be replaced with @seed-design/css/breakpoints when responsive styling lands. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(docs): apply chromatic viewport modes to layout stories Add chromatic: { modes: VIEWPORT_MODES } to Box, Flex, Grid, Stack stories to establish visual snapshot baselines across breakpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(css, react): add breakpoint constants and ResponsiveValue type Introduce viewport breakpoint system for responsive styling support: - Add @seed-design/css/breakpoints with breakpoint names, values, and media queries - Add ResponsiveValue<T> type and isResponsiveObject type guard in react package - Export ResponsiveValue type from @seed-design/react public API Breakpoints: base(0), sm(480), md(768), lg(1280), xl(1440) DES-1314, DES-1318 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(postcss-responsive): add PostCSS plugin for responsive CSS variable cascade chains Introduce postcss-responsive plugin that transforms `--responsive` marker suffixed CSS custom properties into breakpoint-aware cascade chains. Register in qvism-preset and add markers to 20 Box style properties (padding, bleed, width/height, display, flexDirection, gap). Generated CSS includes per-breakpoint fallback vars and @media overrides. Adapted from Reshaped (MIT) postcss.responsive.mjs DES-1324, DES-1317 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> # Conflicts: # packages/css/all.layered.min.css # packages/css/all.min.css * feat(react): add responsive value support to useStyleProps Extend useStyleProps to accept ResponsiveValue<T> for layout-related props (width, height, padding, bleed, display, flexDirection, gap). Single values now output -base suffixed CSS variables to work with the postcss-responsive cascade chain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(react): fix StyleProps consumer compatibility for responsive values - Icon: use UnwrapResponsive<StyleProps["height"]> since --seed-icon-size is outside the --seed-box-* cascade chain (no responsive icon sizing yet) - ResponsivePair: replace handleDimension(gap) with var(--seed-box-gap) CSS variable reference to support responsive gap values - Export UnwrapResponsive utility type for external consumers - Unexport handlePaddingWithSafeArea (internal only) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(react): add responsive utility props for layout components Add responsive Grid columns/rows, hideFrom/showFrom visibility controls for Box/Flex/Grid, and vStackFrom/hStackFrom direction switching for Stack components. Export BreakpointThreshold type for consumers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor * fix(react): move Grid display from .seed-grid CSS to Box's --seed-box-display Previously .seed-grid hardcoded display:grid, which silently overrode --seed-box-display and broke <Grid display="none">. Now Grid passes display="grid" through Box like Flex does, unifying the pattern and enabling hideFrom/showFrom to work correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(react): add JS runtime responsive hooks Add useBreakpoint, useBreakpointValue hooks and optional BreakpointProvider for JS-level responsive behavior (variant switching, conditional rendering). - Module-level singleton matchMedia store with lazy init and ref counting - useSyncExternalStore for tearing-safe concurrent mode support - Hybrid: works without provider, optional BreakpointProvider for SSR defaults - Cascade resolution matching CSS min-width semantics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(react): replace resolveDisplay with generic resolveThreshold Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(react): restructure visibility utils with internal resolveThreshold - resolveThreshold is now a generic internal utility (not exported) - resolveDisplay wraps it with display-specific API ({base, hideFrom, showFrom}) - hideFrom takes precedence if both hideFrom and showFrom are provided Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(react): implement hideFrom via CSS variable cascade, remove showFrom - hideFrom now directly injects `--seed-box-display-{bp}: none` into inline style, leveraging the existing CSS variable cascade chain. This makes hideFrom completely independent of the user's display prop. - Remove showFrom from all components (Box, Flex, Grid) due to fundamental conflict with user's display prop — restoring the original display value at a breakpoint requires knowing what it is. - Remove visibility.ts utility (no longer needed). - Restore Flex/Grid to dev-branch pattern: hardcoded display default with ...rest spread allowing user override. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(react): allow responsive display values on Flex and Grid Wrap Flex `display` and Grid `display` prop types with ResponsiveValue<> so users can pass responsive objects directly: <Flex display={{ base: "none", md: "flex" }} /> <Grid display={{ base: "none", lg: "grid" }} /> This naturally covers the showFrom use case without a dedicated prop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(react): remove vStackFrom/hStackFrom from Stack components Deferred to gradual introduction; users can use Flex with responsive flexDirection directly for the same effect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add comment * fix: better type ResponsiveObject<T> * chore: format * docs * docs * test: add tests for multiple viewports # Conflicts: # docs/stories/utils/parameters.ts * test: add nested layout composition stories for Box Add layout and nested stacks conditions (both static and responsive) to Box stories for visual regression testing with Chromatic viewport modes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: generate * fix: fix recipes using box variables * fix: fix ResponsivePair inline styles * fix: revert changes in action button recipe since it doesn't have seed-box class * fix: bleed* can take responsive objects * fix(css, qvism-preset): use --responsive marker in recipes for style prop override Recipes that expose style props via Pick<StyleProps, ...> need their own CSS variable cascade chain so that resolveResponsive's -base suffix inline styles can override recipe defaults. Without .seed-box class on these components, the cascade chain from base.css is not available. Using --responsive marker lets postcss-responsive generate the full cascade chain (-base, -sm, -md, -lg, -xl + media queries) directly in each recipe's CSS output. Affected recipes: skeleton, bottom-sheet, help-bubble, list-item Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: fix reponsivepair * fix * docs * test: remove unrelated case * feat: add gap fallback to responsivepair * fix: remove unsafe casting * test: add ResponsivePair stories * docs: changeset * feat(qvism-preset): add breakpoint helper for recipes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> # Conflicts: # docs/stories/utils/parameters.ts
1 parent 59a6524 commit a3780cd

70 files changed

Lines changed: 3555 additions & 322 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/responsive-styling.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@seed-design/css": minor
3+
"@seed-design/react": minor
4+
---
5+
6+
Breakpoint 기반 반응형 스타일링을 지원합니다.
7+
8+
- Box, Flex, Grid, VStack 등 유틸리티 컴포넌트의 레이아웃 관련 프로퍼티에 breakpoint 기반 반응형 객체를 사용할 수 있습니다.
9+
10+
```tsx
11+
<Box padding={{ base: "x3", md: "x6" }} />
12+
<Grid columns={{ base: 1, md: 2, lg: 4 }} gap="x4" />
13+
```
14+
15+
- `@seed-design/react`에서 `useBreakpoint` 훅과 `useBreakpointValue` 훅을 제공합니다.
16+
- `useBreakpoint()` — 현재 활성 breakpoint 이름을 반환합니다. (`"base"` | `"sm"` | `"md"` | `"lg"` | `"xl"`)
17+
- `useBreakpointValue(values)` — 반응형 객체에서 현재 breakpoint에 해당하는 값을 반환합니다.
18+
19+
```tsx
20+
const actionButtonProps = useBreakpointValue<ActionButtonProps>({
21+
base: { variant: "neutralWeak" },
22+
lg: { variant: "brandSolid" },
23+
});
24+
```
25+
26+
- `<Grid display="none">`으로 Grid를 숨길 수 없던 문제를 수정합니다.

bun.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/.storybook/preview.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22
import "@seed-design/css/all.css";
33

44
import type { Preview } from "@storybook/nextjs";
5+
import { breakpoints } from "@seed-design/css/breakpoints";
6+
import { ViewportMap } from "storybook/viewport";
7+
8+
const breakpointValues = Object.values(breakpoints);
9+
10+
const viewportMap: ViewportMap = Object.fromEntries(
11+
Object.entries(breakpoints).map(([key, value], i) => {
12+
const nextValue = breakpointValues[i + 1];
13+
const w = nextValue != null ? nextValue - 10 : value + 150;
14+
15+
return [
16+
key,
17+
{
18+
name: `${w}px (>= ${key} (${value}px))`,
19+
styles: {
20+
width: `${w}px`,
21+
height: `${Math.round(w > 768 ? (w * 9) / 16 : (w * 18.5) / 9)}px`,
22+
},
23+
},
24+
];
25+
}),
26+
);
527

628
// TODO: Replace with @seed-design/css/breakpoints when responsive styling lands
729
const breakpoints = { base: 0, sm: 480, md: 768, lg: 1280, xl: 1440 };

docs/content/react/components/(foundation)/layout/box.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,11 @@ import { Box } from "@seed-design/react";
3838
}
3939
```
4040
</ComponentExample>
41+
42+
## Responsive Design
43+
44+
<Card title="Responsive Design" href="/react/components/concepts/responsive-design">
45+
46+
Box의 레이아웃 프로퍼티에 반응형 값을 전달하여 viewport 크기에 따라 스타일을 조정할 수 있습니다.
47+
48+
</Card>

docs/content/react/components/(foundation)/layout/flex.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ description: Flex 컴포넌트는 flexbox를 사용하며 디자인 토큰을 JS
1515
## Props
1616

1717
<react-type-table path="../packages/react/src/components/Flex/Flex.tsx" name="FlexProps" />
18+
19+
## Responsive Design
20+
21+
<Card title="Responsive Design" href="/react/components/concepts/responsive-design">
22+
23+
Flex의 레이아웃 프로퍼티에 반응형 값을 전달하여 viewport 크기에 따라 스타일을 조정할 수 있습니다.
24+
25+
</Card>

docs/content/react/components/(foundation)/layout/grid.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,11 @@ description: Grid 컴포넌트는 CSS Grid를 사용하며 디자인 토큰을 J
127127
<Box gridColumn="2 / 4">colStart=2 colEnd=4</Box>
128128
</Grid>
129129
```
130+
131+
## Responsive Design
132+
133+
<Card title="Responsive Design" href="/react/components/concepts/responsive-design">
134+
135+
Grid의 `columns`, `rows`, `gap` 등에 반응형 값을 전달하여 viewport 크기에 따라 레이아웃을 조정할 수 있습니다.
136+
137+
</Card>

docs/content/react/components/(foundation)/layout/h-stack.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,11 @@ import { HStack } from "@seed-design/react";
2626
## Props
2727

2828
<react-type-table path="../packages/react/src/components/Stack/Stack.tsx" name="HStackProps" />
29+
30+
## Responsive Design
31+
32+
<Card title="Responsive Design" href="/react/components/concepts/responsive-design">
33+
34+
HStack의 레이아웃 프로퍼티에 반응형 값을 전달하여 viewport 크기에 따라 스타일을 조정할 수 있습니다.
35+
36+
</Card>

docs/content/react/components/(foundation)/layout/v-stack.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,11 @@ import { VStack } from "@seed-design/react";
2626
## Props
2727

2828
<react-type-table path="../packages/react/src/components/Stack/Stack.tsx" name="VStackProps" />
29+
30+
## Responsive Design
31+
32+
<Card title="Responsive Design" href="/react/components/concepts/responsive-design">
33+
34+
VStack의 레이아웃 프로퍼티에 반응형 값을 전달하여 viewport 크기에 따라 스타일을 조정할 수 있습니다.
35+
36+
</Card>
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
---
2+
title: Responsive Design
3+
description: Viewport 크기에 따라 레이아웃과 스타일을 조정하는 방법을 알아봅니다.
4+
---
5+
6+
SEED Design은 mobile-first 반응형 시스템을 제공합니다. 특정 breakpoint에 값을 지정하면 더 넓은 viewport에도 동일한 값이 적용됩니다.
7+
8+
## Breakpoints
9+
10+
| 이름 | `min-width` |
11+
| ------ | ----------- |
12+
| `base` | 0px |
13+
| `sm` | 480px |
14+
| `md` | 768px |
15+
| `lg` | 1280px |
16+
| `xl` | 1440px |
17+
18+
## 레이아웃 컴포넌트에서 반응형 Prop 사용하기
19+
20+
`Box` 및 Box 기반 컴포넌트(`Flex`, `Grid`, `VStack`, `HStack` 등)에서 레이아웃 프로퍼티의 값으로 breakpoint 이름을 키로 사용하는 객체를 받을 수 있습니다.
21+
22+
```tsx
23+
<Box padding={{ base: "x3", md: "x4" }} />
24+
```
25+
26+
| 분류 | 프로퍼티 |
27+
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
28+
| **Layout** | `display`, `flexDirection`, `gap` |
29+
| **Sizing** | `width`, `minWidth`, `maxWidth`, `height`, `minHeight`, `maxHeight` |
30+
| **Padding** | `padding`, `paddingX`, `paddingY`, `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft`, `p`, `px`, `py`, `pt`, `pr`, `pb`, `pl` |
31+
| **Bleed** | `bleedX`, `bleedY`, `bleedTop`, `bleedRight`, `bleedBottom`, `bleedLeft` |
32+
33+
<ComponentExample name="react/responsive-design/responsive-props">
34+
```json doc-gen:file
35+
{
36+
"file": "examples/react/responsive-design/responsive-props.tsx",
37+
"codeblock": true
38+
}
39+
```
40+
</ComponentExample>
41+
42+
<Cards>
43+
<Card title="Box" href="/react/components/layout/box">
44+
Box 컴포넌트는 가장 기초적인 레이아웃 컴포넌트입니다. 디자인 토큰을 JSX에서
45+
사용할 수 있도록 도와줍니다.
46+
</Card>
47+
<Card title="Flex" href="/react/components/layout/flex">
48+
Flex 컴포넌트는 flexbox를 사용하며 디자인 토큰을 JSX에서 사용할 수 있도록
49+
도와줍니다.
50+
</Card>
51+
<Card title="Grid" href="/react/components/layout/grid">
52+
Grid 컴포넌트는 CSS Grid를 사용하며 디자인 토큰을 JSX에서 사용할 수 있도록
53+
도와줍니다.
54+
</Card>
55+
<Card title="VStack" href="/react/components/layout/v-stack">
56+
세로로 쌓이는 레이아웃을 구성합니다. 디자인 토큰을 JSX에서 사용할 수 있도록
57+
도와줍니다.
58+
</Card>
59+
<Card title="HStack" href="/react/components/layout/h-stack">
60+
가로로 쌓이는 레이아웃을 구성합니다. 디자인 토큰을 JSX에서 사용할 수 있도록
61+
도와줍니다.
62+
</Card>
63+
</Cards>
64+
65+
### Hiding & Showing Elements
66+
67+
`display`에 반응형 값을 전달하여 특정 breakpoint에서 요소를 표시하거나 숨길 수 있습니다.
68+
69+
<ComponentExample name="react/responsive-design/responsive-display">
70+
```json doc-gen:file
71+
{
72+
"file": "examples/react/responsive-design/responsive-display.tsx",
73+
"codeblock": true
74+
}
75+
```
76+
</ComponentExample>
77+
78+
#### `hideFrom`
79+
80+
`hideFrom`을 사용하여 특정 breakpoint 이상에서 요소를 숨깁니다. (`display: none;`)
81+
82+
`Box` 및 Box 기반 컴포넌트(`Flex`, `Grid`, `VStack`, `HStack` 등)에서 사용할 수 있습니다.
83+
84+
`hideFrom="md"``display={{ md: "none" }}`과 동일합니다.
85+
86+
<ComponentExample name="react/responsive-design/hide-from">
87+
```json doc-gen:file
88+
{
89+
"file": "examples/react/responsive-design/hide-from.tsx",
90+
"codeblock": true
91+
}
92+
```
93+
</ComponentExample>
94+
95+
### Grid
96+
97+
Breakpoint 별로 `columns``rows` 속성을 지정할 수 있습니다.
98+
99+
<ComponentExample name="react/responsive-design/responsive-grid">
100+
```json doc-gen:file
101+
{
102+
"file": "examples/react/responsive-design/responsive-grid.tsx",
103+
"codeblock": true
104+
}
105+
```
106+
</ComponentExample>
107+
108+
<Card title="Grid" href="/react/components/layout/grid">
109+
Grid 컴포넌트는 CSS Grid를 사용하며 디자인 토큰을 JSX에서 사용할 수 있도록
110+
도와줍니다.
111+
</Card>
112+
113+
## Hooks
114+
115+
`window.matchMedia`를 기반으로 구현된 `useBreakpoint``useBreakpointValue` 훅을 제공합니다.
116+
117+
가능한 경우 항상 CSS 기반으로 작동하는 반응형 prop을 사용하고, 훅은 JS 로직이 필요한 경우에만 사용하세요.
118+
119+
### `useBreakpoint`
120+
121+
현재 속한 breakpoint 이름을 반환합니다.
122+
123+
<ComponentExample name="react/responsive-design/use-breakpoint">
124+
```json doc-gen:file
125+
{
126+
"file": "examples/react/responsive-design/use-breakpoint.tsx",
127+
"codeblock": true
128+
}
129+
```
130+
</ComponentExample>
131+
132+
### `useBreakpointValue`
133+
134+
파라미터로 전달한 반응형 값 객체를 현재 viewport에 맞는 값으로 resolve합니다.
135+
136+
<ComponentExample name="react/responsive-design/use-breakpoint-value">
137+
```json doc-gen:file
138+
{
139+
"file": "examples/react/responsive-design/use-breakpoint-value.tsx",
140+
"codeblock": true
141+
}
142+
```
143+
</ComponentExample>
144+
145+
### Server-Side Rendering
146+
147+
SSR 등 viewport 정보를 알 수 없는 환경에서 훅 사용 시 적용될 breakpoint 기본값을 정의하려면 `BreakpointProvider`를 통해 `defaultBreakpoint`를 지정하세요.
148+
149+
`BreakpointProvider` 밖에서 훅을 호출했으나 `window.matchMedia`를 사용할 수 없는 경우, `useBreakpoint``base`를, `useBreakpointValue``base` key의 값을 반환합니다.
150+
151+
```tsx
152+
import { BreakpointProvider } from "@seed-design/react";
153+
154+
function App() {
155+
return (
156+
<BreakpointProvider defaultBreakpoint="md">
157+
<MyApp />
158+
</BreakpointProvider>
159+
);
160+
}
161+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Box, VStack, Text } from "@seed-design/react";
2+
3+
export default function HideFromExample() {
4+
return (
5+
<VStack gap="x2" align="center">
6+
<Text textStyle="t3Medium">아래 Box는 xl breakpoint 이상에서는 숨겨집니다.</Text>
7+
<Box
8+
bg="bg.informativeWeak"
9+
color="fg.informativeContrast"
10+
padding="x4"
11+
borderRadius="r3"
12+
hideFrom="xl"
13+
className="font-mono"
14+
>
15+
hideFrom="xl"
16+
</Box>
17+
</VStack>
18+
);
19+
}

0 commit comments

Comments
 (0)