@x-plat/design-system 0.5.45 → 0.5.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,6 +66,7 @@ var Swiper = (props) => {
66
66
  spaceBetween = 16,
67
67
  showProgress = false,
68
68
  autoplayDelay = 3e3,
69
+ pauseOnInteraction = 0,
69
70
  speed = 300,
70
71
  slideBy = 1,
71
72
  index: indexProp,
@@ -93,6 +94,8 @@ var Swiper = (props) => {
93
94
  const startXRef = import_react.default.useRef(0);
94
95
  const startTimeRef = import_react.default.useRef(0);
95
96
  const autoplayTimerRef = import_react.default.useRef(null);
97
+ const resumeTimeoutRef = import_react.default.useRef(null);
98
+ const [paused, setPaused] = import_react.default.useState(false);
96
99
  import_react.default.useEffect(() => {
97
100
  const el = containerRef.current;
98
101
  if (!el) return;
@@ -192,15 +195,36 @@ var Swiper = (props) => {
192
195
  slideTo
193
196
  }));
194
197
  import_react.default.useEffect(() => {
195
- if (!auto || !canSlide) return;
198
+ if (!auto || !canSlide || paused) return;
196
199
  autoplayTimerRef.current = setInterval(slideNext, autoplayDelay);
197
200
  return () => {
198
201
  if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
199
202
  };
200
- }, [auto, autoplayDelay, slideNext, canSlide]);
201
- const pauseAutoplay = () => {
202
- if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
203
+ }, [auto, autoplayDelay, slideNext, canSlide, paused]);
204
+ const pauseForInteraction = () => {
205
+ setPaused(true);
206
+ if (resumeTimeoutRef.current) {
207
+ clearTimeout(resumeTimeoutRef.current);
208
+ resumeTimeoutRef.current = null;
209
+ }
210
+ };
211
+ const scheduleAutoplayResume = () => {
212
+ if (!auto) return;
213
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
214
+ if (pauseOnInteraction <= 0) {
215
+ setPaused(false);
216
+ return;
217
+ }
218
+ resumeTimeoutRef.current = setTimeout(() => {
219
+ setPaused(false);
220
+ resumeTimeoutRef.current = null;
221
+ }, pauseOnInteraction);
203
222
  };
223
+ import_react.default.useEffect(() => {
224
+ return () => {
225
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
226
+ };
227
+ }, []);
204
228
  const getClientX = (e) => {
205
229
  if ("touches" in e) return e.touches[0]?.clientX ?? 0;
206
230
  return e.clientX;
@@ -208,7 +232,7 @@ var Swiper = (props) => {
208
232
  const handleDragStart = (e) => {
209
233
  if (!canSlide) return;
210
234
  if ("button" in e && e.button !== 0) return;
211
- pauseAutoplay();
235
+ pauseForInteraction();
212
236
  setIsDragging(true);
213
237
  setAnimated(false);
214
238
  startXRef.current = getClientX(e);
@@ -238,6 +262,7 @@ var Swiper = (props) => {
238
262
  setAnimated(true);
239
263
  }
240
264
  setDragOffset(0);
265
+ scheduleAutoplayResume();
241
266
  };
242
267
  window.addEventListener("mousemove", handleMove);
243
268
  window.addEventListener("mouseup", handleEnd);
@@ -25,6 +25,8 @@ interface SwiperProps {
25
25
  showProgress?: boolean;
26
26
  /** 오토플레이 딜레이 (ms) */
27
27
  autoplayDelay?: number;
28
+ /** 사용자 인터랙션(드래그/탭) 후 autoplay 가 재개되기까지 대기 시간 (ms). 0 이면 즉시 재개. 기본 0 */
29
+ pauseOnInteraction?: number;
28
30
  /** 슬라이드 전환 속도 (ms) */
29
31
  speed?: number;
30
32
  /** 한 번에 이동할 슬라이드 수 (기본: 1) */
@@ -25,6 +25,8 @@ interface SwiperProps {
25
25
  showProgress?: boolean;
26
26
  /** 오토플레이 딜레이 (ms) */
27
27
  autoplayDelay?: number;
28
+ /** 사용자 인터랙션(드래그/탭) 후 autoplay 가 재개되기까지 대기 시간 (ms). 0 이면 즉시 재개. 기본 0 */
29
+ pauseOnInteraction?: number;
28
30
  /** 슬라이드 전환 속도 (ms) */
29
31
  speed?: number;
30
32
  /** 한 번에 이동할 슬라이드 수 (기본: 1) */
@@ -30,6 +30,7 @@ var Swiper = (props) => {
30
30
  spaceBetween = 16,
31
31
  showProgress = false,
32
32
  autoplayDelay = 3e3,
33
+ pauseOnInteraction = 0,
33
34
  speed = 300,
34
35
  slideBy = 1,
35
36
  index: indexProp,
@@ -57,6 +58,8 @@ var Swiper = (props) => {
57
58
  const startXRef = React.useRef(0);
58
59
  const startTimeRef = React.useRef(0);
59
60
  const autoplayTimerRef = React.useRef(null);
61
+ const resumeTimeoutRef = React.useRef(null);
62
+ const [paused, setPaused] = React.useState(false);
60
63
  React.useEffect(() => {
61
64
  const el = containerRef.current;
62
65
  if (!el) return;
@@ -156,15 +159,36 @@ var Swiper = (props) => {
156
159
  slideTo
157
160
  }));
158
161
  React.useEffect(() => {
159
- if (!auto || !canSlide) return;
162
+ if (!auto || !canSlide || paused) return;
160
163
  autoplayTimerRef.current = setInterval(slideNext, autoplayDelay);
161
164
  return () => {
162
165
  if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
163
166
  };
164
- }, [auto, autoplayDelay, slideNext, canSlide]);
165
- const pauseAutoplay = () => {
166
- if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
167
+ }, [auto, autoplayDelay, slideNext, canSlide, paused]);
168
+ const pauseForInteraction = () => {
169
+ setPaused(true);
170
+ if (resumeTimeoutRef.current) {
171
+ clearTimeout(resumeTimeoutRef.current);
172
+ resumeTimeoutRef.current = null;
173
+ }
174
+ };
175
+ const scheduleAutoplayResume = () => {
176
+ if (!auto) return;
177
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
178
+ if (pauseOnInteraction <= 0) {
179
+ setPaused(false);
180
+ return;
181
+ }
182
+ resumeTimeoutRef.current = setTimeout(() => {
183
+ setPaused(false);
184
+ resumeTimeoutRef.current = null;
185
+ }, pauseOnInteraction);
167
186
  };
187
+ React.useEffect(() => {
188
+ return () => {
189
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
190
+ };
191
+ }, []);
168
192
  const getClientX = (e) => {
169
193
  if ("touches" in e) return e.touches[0]?.clientX ?? 0;
170
194
  return e.clientX;
@@ -172,7 +196,7 @@ var Swiper = (props) => {
172
196
  const handleDragStart = (e) => {
173
197
  if (!canSlide) return;
174
198
  if ("button" in e && e.button !== 0) return;
175
- pauseAutoplay();
199
+ pauseForInteraction();
176
200
  setIsDragging(true);
177
201
  setAnimated(false);
178
202
  startXRef.current = getClientX(e);
@@ -202,6 +226,7 @@ var Swiper = (props) => {
202
226
  setAnimated(true);
203
227
  }
204
228
  setDragOffset(0);
229
+ scheduleAutoplayResume();
205
230
  };
206
231
  window.addEventListener("mousemove", handleMove);
207
232
  window.addEventListener("mouseup", handleEnd);
@@ -4712,6 +4712,7 @@ var Swiper = (props) => {
4712
4712
  spaceBetween = 16,
4713
4713
  showProgress = false,
4714
4714
  autoplayDelay = 3e3,
4715
+ pauseOnInteraction = 0,
4715
4716
  speed = 300,
4716
4717
  slideBy = 1,
4717
4718
  index: indexProp,
@@ -4739,6 +4740,8 @@ var Swiper = (props) => {
4739
4740
  const startXRef = import_react31.default.useRef(0);
4740
4741
  const startTimeRef = import_react31.default.useRef(0);
4741
4742
  const autoplayTimerRef = import_react31.default.useRef(null);
4743
+ const resumeTimeoutRef = import_react31.default.useRef(null);
4744
+ const [paused, setPaused] = import_react31.default.useState(false);
4742
4745
  import_react31.default.useEffect(() => {
4743
4746
  const el = containerRef.current;
4744
4747
  if (!el) return;
@@ -4838,15 +4841,36 @@ var Swiper = (props) => {
4838
4841
  slideTo
4839
4842
  }));
4840
4843
  import_react31.default.useEffect(() => {
4841
- if (!auto || !canSlide) return;
4844
+ if (!auto || !canSlide || paused) return;
4842
4845
  autoplayTimerRef.current = setInterval(slideNext, autoplayDelay);
4843
4846
  return () => {
4844
4847
  if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
4845
4848
  };
4846
- }, [auto, autoplayDelay, slideNext, canSlide]);
4847
- const pauseAutoplay = () => {
4848
- if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
4849
+ }, [auto, autoplayDelay, slideNext, canSlide, paused]);
4850
+ const pauseForInteraction = () => {
4851
+ setPaused(true);
4852
+ if (resumeTimeoutRef.current) {
4853
+ clearTimeout(resumeTimeoutRef.current);
4854
+ resumeTimeoutRef.current = null;
4855
+ }
4856
+ };
4857
+ const scheduleAutoplayResume = () => {
4858
+ if (!auto) return;
4859
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
4860
+ if (pauseOnInteraction <= 0) {
4861
+ setPaused(false);
4862
+ return;
4863
+ }
4864
+ resumeTimeoutRef.current = setTimeout(() => {
4865
+ setPaused(false);
4866
+ resumeTimeoutRef.current = null;
4867
+ }, pauseOnInteraction);
4849
4868
  };
4869
+ import_react31.default.useEffect(() => {
4870
+ return () => {
4871
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
4872
+ };
4873
+ }, []);
4850
4874
  const getClientX = (e) => {
4851
4875
  if ("touches" in e) return e.touches[0]?.clientX ?? 0;
4852
4876
  return e.clientX;
@@ -4854,7 +4878,7 @@ var Swiper = (props) => {
4854
4878
  const handleDragStart = (e) => {
4855
4879
  if (!canSlide) return;
4856
4880
  if ("button" in e && e.button !== 0) return;
4857
- pauseAutoplay();
4881
+ pauseForInteraction();
4858
4882
  setIsDragging(true);
4859
4883
  setAnimated(false);
4860
4884
  startXRef.current = getClientX(e);
@@ -4884,6 +4908,7 @@ var Swiper = (props) => {
4884
4908
  setAnimated(true);
4885
4909
  }
4886
4910
  setDragOffset(0);
4911
+ scheduleAutoplayResume();
4887
4912
  };
4888
4913
  window.addEventListener("mousemove", handleMove);
4889
4914
  window.addEventListener("mouseup", handleEnd);
@@ -4622,6 +4622,7 @@ var Swiper = (props) => {
4622
4622
  spaceBetween = 16,
4623
4623
  showProgress = false,
4624
4624
  autoplayDelay = 3e3,
4625
+ pauseOnInteraction = 0,
4625
4626
  speed = 300,
4626
4627
  slideBy = 1,
4627
4628
  index: indexProp,
@@ -4649,6 +4650,8 @@ var Swiper = (props) => {
4649
4650
  const startXRef = React30.useRef(0);
4650
4651
  const startTimeRef = React30.useRef(0);
4651
4652
  const autoplayTimerRef = React30.useRef(null);
4653
+ const resumeTimeoutRef = React30.useRef(null);
4654
+ const [paused, setPaused] = React30.useState(false);
4652
4655
  React30.useEffect(() => {
4653
4656
  const el = containerRef.current;
4654
4657
  if (!el) return;
@@ -4748,15 +4751,36 @@ var Swiper = (props) => {
4748
4751
  slideTo
4749
4752
  }));
4750
4753
  React30.useEffect(() => {
4751
- if (!auto || !canSlide) return;
4754
+ if (!auto || !canSlide || paused) return;
4752
4755
  autoplayTimerRef.current = setInterval(slideNext, autoplayDelay);
4753
4756
  return () => {
4754
4757
  if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
4755
4758
  };
4756
- }, [auto, autoplayDelay, slideNext, canSlide]);
4757
- const pauseAutoplay = () => {
4758
- if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
4759
+ }, [auto, autoplayDelay, slideNext, canSlide, paused]);
4760
+ const pauseForInteraction = () => {
4761
+ setPaused(true);
4762
+ if (resumeTimeoutRef.current) {
4763
+ clearTimeout(resumeTimeoutRef.current);
4764
+ resumeTimeoutRef.current = null;
4765
+ }
4766
+ };
4767
+ const scheduleAutoplayResume = () => {
4768
+ if (!auto) return;
4769
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
4770
+ if (pauseOnInteraction <= 0) {
4771
+ setPaused(false);
4772
+ return;
4773
+ }
4774
+ resumeTimeoutRef.current = setTimeout(() => {
4775
+ setPaused(false);
4776
+ resumeTimeoutRef.current = null;
4777
+ }, pauseOnInteraction);
4759
4778
  };
4779
+ React30.useEffect(() => {
4780
+ return () => {
4781
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
4782
+ };
4783
+ }, []);
4760
4784
  const getClientX = (e) => {
4761
4785
  if ("touches" in e) return e.touches[0]?.clientX ?? 0;
4762
4786
  return e.clientX;
@@ -4764,7 +4788,7 @@ var Swiper = (props) => {
4764
4788
  const handleDragStart = (e) => {
4765
4789
  if (!canSlide) return;
4766
4790
  if ("button" in e && e.button !== 0) return;
4767
- pauseAutoplay();
4791
+ pauseForInteraction();
4768
4792
  setIsDragging(true);
4769
4793
  setAnimated(false);
4770
4794
  startXRef.current = getClientX(e);
@@ -4794,6 +4818,7 @@ var Swiper = (props) => {
4794
4818
  setAnimated(true);
4795
4819
  }
4796
4820
  setDragOffset(0);
4821
+ scheduleAutoplayResume();
4797
4822
  };
4798
4823
  window.addEventListener("mousemove", handleMove);
4799
4824
  window.addEventListener("mouseup", handleEnd);
package/dist/index.cjs CHANGED
@@ -9124,6 +9124,7 @@ var Swiper = (props) => {
9124
9124
  spaceBetween = 16,
9125
9125
  showProgress = false,
9126
9126
  autoplayDelay = 3e3,
9127
+ pauseOnInteraction = 0,
9127
9128
  speed = 300,
9128
9129
  slideBy = 1,
9129
9130
  index: indexProp,
@@ -9151,6 +9152,8 @@ var Swiper = (props) => {
9151
9152
  const startXRef = import_react31.default.useRef(0);
9152
9153
  const startTimeRef = import_react31.default.useRef(0);
9153
9154
  const autoplayTimerRef = import_react31.default.useRef(null);
9155
+ const resumeTimeoutRef = import_react31.default.useRef(null);
9156
+ const [paused, setPaused] = import_react31.default.useState(false);
9154
9157
  import_react31.default.useEffect(() => {
9155
9158
  const el = containerRef.current;
9156
9159
  if (!el) return;
@@ -9250,15 +9253,36 @@ var Swiper = (props) => {
9250
9253
  slideTo
9251
9254
  }));
9252
9255
  import_react31.default.useEffect(() => {
9253
- if (!auto || !canSlide) return;
9256
+ if (!auto || !canSlide || paused) return;
9254
9257
  autoplayTimerRef.current = setInterval(slideNext, autoplayDelay);
9255
9258
  return () => {
9256
9259
  if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
9257
9260
  };
9258
- }, [auto, autoplayDelay, slideNext, canSlide]);
9259
- const pauseAutoplay = () => {
9260
- if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
9261
+ }, [auto, autoplayDelay, slideNext, canSlide, paused]);
9262
+ const pauseForInteraction = () => {
9263
+ setPaused(true);
9264
+ if (resumeTimeoutRef.current) {
9265
+ clearTimeout(resumeTimeoutRef.current);
9266
+ resumeTimeoutRef.current = null;
9267
+ }
9268
+ };
9269
+ const scheduleAutoplayResume = () => {
9270
+ if (!auto) return;
9271
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
9272
+ if (pauseOnInteraction <= 0) {
9273
+ setPaused(false);
9274
+ return;
9275
+ }
9276
+ resumeTimeoutRef.current = setTimeout(() => {
9277
+ setPaused(false);
9278
+ resumeTimeoutRef.current = null;
9279
+ }, pauseOnInteraction);
9261
9280
  };
9281
+ import_react31.default.useEffect(() => {
9282
+ return () => {
9283
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
9284
+ };
9285
+ }, []);
9262
9286
  const getClientX = (e) => {
9263
9287
  if ("touches" in e) return e.touches[0]?.clientX ?? 0;
9264
9288
  return e.clientX;
@@ -9266,7 +9290,7 @@ var Swiper = (props) => {
9266
9290
  const handleDragStart = (e) => {
9267
9291
  if (!canSlide) return;
9268
9292
  if ("button" in e && e.button !== 0) return;
9269
- pauseAutoplay();
9293
+ pauseForInteraction();
9270
9294
  setIsDragging(true);
9271
9295
  setAnimated(false);
9272
9296
  startXRef.current = getClientX(e);
@@ -9296,6 +9320,7 @@ var Swiper = (props) => {
9296
9320
  setAnimated(true);
9297
9321
  }
9298
9322
  setDragOffset(0);
9323
+ scheduleAutoplayResume();
9299
9324
  };
9300
9325
  window.addEventListener("mousemove", handleMove);
9301
9326
  window.addEventListener("mouseup", handleEnd);
package/dist/index.js CHANGED
@@ -8725,6 +8725,7 @@ var Swiper = (props) => {
8725
8725
  spaceBetween = 16,
8726
8726
  showProgress = false,
8727
8727
  autoplayDelay = 3e3,
8728
+ pauseOnInteraction = 0,
8728
8729
  speed = 300,
8729
8730
  slideBy = 1,
8730
8731
  index: indexProp,
@@ -8752,6 +8753,8 @@ var Swiper = (props) => {
8752
8753
  const startXRef = React30.useRef(0);
8753
8754
  const startTimeRef = React30.useRef(0);
8754
8755
  const autoplayTimerRef = React30.useRef(null);
8756
+ const resumeTimeoutRef = React30.useRef(null);
8757
+ const [paused, setPaused] = React30.useState(false);
8755
8758
  React30.useEffect(() => {
8756
8759
  const el = containerRef.current;
8757
8760
  if (!el) return;
@@ -8851,15 +8854,36 @@ var Swiper = (props) => {
8851
8854
  slideTo
8852
8855
  }));
8853
8856
  React30.useEffect(() => {
8854
- if (!auto || !canSlide) return;
8857
+ if (!auto || !canSlide || paused) return;
8855
8858
  autoplayTimerRef.current = setInterval(slideNext, autoplayDelay);
8856
8859
  return () => {
8857
8860
  if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
8858
8861
  };
8859
- }, [auto, autoplayDelay, slideNext, canSlide]);
8860
- const pauseAutoplay = () => {
8861
- if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current);
8862
+ }, [auto, autoplayDelay, slideNext, canSlide, paused]);
8863
+ const pauseForInteraction = () => {
8864
+ setPaused(true);
8865
+ if (resumeTimeoutRef.current) {
8866
+ clearTimeout(resumeTimeoutRef.current);
8867
+ resumeTimeoutRef.current = null;
8868
+ }
8869
+ };
8870
+ const scheduleAutoplayResume = () => {
8871
+ if (!auto) return;
8872
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
8873
+ if (pauseOnInteraction <= 0) {
8874
+ setPaused(false);
8875
+ return;
8876
+ }
8877
+ resumeTimeoutRef.current = setTimeout(() => {
8878
+ setPaused(false);
8879
+ resumeTimeoutRef.current = null;
8880
+ }, pauseOnInteraction);
8862
8881
  };
8882
+ React30.useEffect(() => {
8883
+ return () => {
8884
+ if (resumeTimeoutRef.current) clearTimeout(resumeTimeoutRef.current);
8885
+ };
8886
+ }, []);
8863
8887
  const getClientX = (e) => {
8864
8888
  if ("touches" in e) return e.touches[0]?.clientX ?? 0;
8865
8889
  return e.clientX;
@@ -8867,7 +8891,7 @@ var Swiper = (props) => {
8867
8891
  const handleDragStart = (e) => {
8868
8892
  if (!canSlide) return;
8869
8893
  if ("button" in e && e.button !== 0) return;
8870
- pauseAutoplay();
8894
+ pauseForInteraction();
8871
8895
  setIsDragging(true);
8872
8896
  setAnimated(false);
8873
8897
  startXRef.current = getClientX(e);
@@ -8897,6 +8921,7 @@ var Swiper = (props) => {
8897
8921
  setAnimated(true);
8898
8922
  }
8899
8923
  setDragOffset(0);
8924
+ scheduleAutoplayResume();
8900
8925
  };
8901
8926
  window.addEventListener("mousemove", handleMove);
8902
8927
  window.addEventListener("mouseup", handleEnd);
@@ -21,6 +21,7 @@
21
21
  2. 컴포넌트는 `width: 100%` 로 부모를 채우므로 **반드시 GridItem 또는 wrapper 로 감싸서 너비를 제어**.
22
22
  3. **CSS 로 DS 컴포넌트를 오버라이드하지 않는다**.
23
23
  4. 색상·간격·타이포는 **semantic 토큰만** 사용. primitive / brand 직접 참조 금지.
24
+ 5. **모든 visible 그룹은 명시적 spacing 필수**. wrapper 의 `padding` / `gap` / 간격을 spacing 토큰으로 지정. 매직 넘버(`16px`, `1rem` 등) 금지.
24
25
 
25
26
  ---
26
27
 
@@ -158,6 +159,185 @@ import { FullGrid, FullScreen, GapGrid, GridItem } from "@x-plat/design-system/l
158
159
 
159
160
  ---
160
161
 
162
+ ## 공간(Spacing) 처리 — **누락 금지**
163
+
164
+ DS 컴포넌트는 자체적으로 spacing 을 가지지 않는다(`width: 100%` 만). **wrapper 가 모든 spacing 을 책임진다.** 누락하면 콘텐츠가 붙어버린다.
165
+
166
+ ### 어디에 적용하는가
167
+
168
+ | 상황 | 처리 |
169
+ |---|---|
170
+ | 컴포넌트들 사이 세로 간격 | wrapper 에 `display: flex; flex-direction: column; gap: var(--spacing-space-N);` |
171
+ | 컴포넌트들 사이 가로 간격 | wrapper 에 `display: flex; gap: var(--spacing-space-N);` 또는 `FullGrid gap` |
172
+ | 컨테이너 내부 padding | wrapper 에 `padding: var(--spacing-space-N);` |
173
+ | 카드/모달 등 박스 내부 | `padding: var(--spacing-space-4);` 가 일반적 |
174
+ | 섹션과 섹션 사이 | `margin-top: var(--spacing-space-6);` 또는 부모 gap |
175
+
176
+ ### 사용 가능한 spacing 토큰 (semantic 만)
177
+
178
+ ```
179
+ --spacing-space-{none|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16}
180
+ --spacing-radius-{none|sm|md|lg|xl|full}
181
+ --spacing-stroke-{none|xs|sm|md|lg|xl|2xl}
182
+ --spacing-control-height-{xs|sm|md|lg}
183
+ ```
184
+
185
+ space-1 = 4px, space-2 = 8px, space-3 = 12px, space-4 = 16px, space-5 = 20px, space-6 = 24px, ...
186
+
187
+ ### 자주 누락되는 케이스 — 반드시 적용
188
+
189
+ DS 컴포넌트는 자체 외부 margin 없음. 부모가 gap 안 주면 모두 붙어버린다. 아래 케이스에서 절대 빼먹지 말 것.
190
+
191
+ #### 1. 섹션 내 카드 목록 (자주 누락 #1)
192
+
193
+ ```tsx
194
+ // ❌ 카드들이 다닥다닥 붙어버림
195
+ <section>
196
+ <Card>...</Card>
197
+ <Card>...</Card>
198
+ <Card>...</Card>
199
+ </section>
200
+
201
+ // ✅ 부모에 gap 필수
202
+ <section style={{
203
+ display: "flex",
204
+ flexDirection: "column",
205
+ gap: "var(--spacing-space-4)", // 카드 간 16px
206
+ }}>
207
+ <Card>...</Card>
208
+ <Card>...</Card>
209
+ <Card>...</Card>
210
+ </section>
211
+
212
+ // ✅ 카드 그리드형 — FullGrid 사용 시 gap prop
213
+ <FullGrid gap={16}>
214
+ <GridItem column={{ default: 4 }}><Card>...</Card></GridItem>
215
+ <GridItem column={{ default: 4 }}><Card>...</Card></GridItem>
216
+ <GridItem column={{ default: 4 }}><Card>...</Card></GridItem>
217
+ </FullGrid>
218
+ ```
219
+
220
+ #### 2. 폼 요소 사이 간격 (자주 누락 #2)
221
+
222
+ ```tsx
223
+ // ❌ Input 들이 붙고 라벨/도움말도 빽빽
224
+ <form>
225
+ <Input label="이름" />
226
+ <Input label="이메일" />
227
+ <Select options={...} />
228
+ <Button type="primary">제출</Button>
229
+ </form>
230
+
231
+ // ✅ form 전체 세로 gap + 버튼 가로 정렬
232
+ <form style={{
233
+ display: "flex",
234
+ flexDirection: "column",
235
+ gap: "var(--spacing-space-4)", // 폼 필드 간 16px
236
+ }}>
237
+ <Input label="이름" />
238
+ <Input label="이메일" />
239
+ <Select options={...} />
240
+ <div style={{
241
+ display: "flex",
242
+ gap: "var(--spacing-space-2)", // 버튼 간 8px
243
+ justifyContent: "flex-end",
244
+ marginTop: "var(--spacing-space-2)", // 폼 필드와 추가 8px (선택)
245
+ }}>
246
+ <Button type="default">취소</Button>
247
+ <Button type="primary">제출</Button>
248
+ </div>
249
+ </form>
250
+ ```
251
+
252
+ #### 3. 페이지 섹션 사이 간격
253
+
254
+ ```tsx
255
+ // ❌ 섹션들이 붙어버림
256
+ <main>
257
+ <section><h2>섹션 A</h2>...</section>
258
+ <section><h2>섹션 B</h2>...</section>
259
+ </main>
260
+
261
+ // ✅ 페이지 padding + 섹션 간 gap
262
+ <main style={{
263
+ display: "flex",
264
+ flexDirection: "column",
265
+ gap: "var(--spacing-space-8)", // 섹션 간 32px
266
+ padding: "var(--spacing-space-6)", // 페이지 padding 24px
267
+ }}>
268
+ <section>...</section>
269
+ <section>...</section>
270
+ </main>
271
+ ```
272
+
273
+ #### 4. 카드 내부 콘텐츠
274
+
275
+ ```tsx
276
+ // Card 컴포넌트 children 직접 배치 시
277
+ <Card>
278
+ <div style={{
279
+ padding: "var(--spacing-space-4)", // 카드 내부 padding 16px
280
+ display: "flex",
281
+ flexDirection: "column",
282
+ gap: "var(--spacing-space-3)", // 콘텐츠 간 12px
283
+ }}>
284
+ <h3>제목</h3>
285
+ <p>본문</p>
286
+ <Button type="primary">액션</Button>
287
+ </div>
288
+ </Card>
289
+ ```
290
+
291
+ #### 5. 가로 액션 버튼 그룹
292
+
293
+ ```tsx
294
+ <div style={{
295
+ display: "flex",
296
+ gap: "var(--spacing-space-2)", // 버튼 간 8px
297
+ }}>
298
+ <Button type="default">취소</Button>
299
+ <Button type="primary">저장</Button>
300
+ </div>
301
+ ```
302
+
303
+ ### 케이스별 권장 spacing 값
304
+
305
+ | 케이스 | 권장 값 |
306
+ |---|---|
307
+ | 폼 필드 간 (vertical) | `space-4` (16px) |
308
+ | 카드 목록 간 (vertical) | `space-3 ~ space-4` (12-16px) |
309
+ | 그리드형 카드 (gap) | `space-4` (16px) |
310
+ | 액션 버튼 간 (horizontal) | `space-2` (8px) |
311
+ | 섹션 간 | `space-6 ~ space-8` (24-32px) |
312
+ | 카드/박스 내부 padding | `space-4 ~ space-5` (16-20px) |
313
+ | 라벨 - 입력 (Input 내부 자동 처리됨) | DS 컴포넌트가 처리 |
314
+ | 페이지 padding | `space-6` (24px) |
315
+
316
+ ### ❌ 금지 패턴
317
+
318
+ ```tsx
319
+ // 매직 넘버 (토큰 안 씀)
320
+ <div style={{ padding: 16, gap: 8 }}>...</div>
321
+
322
+ // 컴포넌트 사이 spacing 없음 (요소들이 붙어버림)
323
+ <div>
324
+ <Button>A</Button>
325
+ <Button>B</Button>
326
+ </div>
327
+
328
+ // primitive 직접 참조 (semantic 만 허용)
329
+ <div style={{ padding: "var(--spacing-size-4)" }}>...</div>
330
+ ```
331
+
332
+ ### 체크리스트 (코드 작성 후 확인)
333
+
334
+ - [ ] 모든 wrapper `<div>` 에 명시적 `gap` 또는 `padding` 이 있는가
335
+ - [ ] spacing 값이 모두 `var(--spacing-space-*)` 등 토큰인가 (숫자 리터럴 금지)
336
+ - [ ] 카드/박스 컴포넌트 내부에 적절한 padding 이 있는가
337
+ - [ ] 섹션 사이에 가시적인 간격이 있는가
338
+
339
+ ---
340
+
161
341
  ## 컴포넌트 사용 패턴
162
342
 
163
343
  ### ✅ 올바른 사용
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-plat/design-system",
3
- "version": "0.5.45",
3
+ "version": "0.5.46",
4
4
  "description": "XPLAT UI Design System",
5
5
  "author": "XPLAT WOONG",
6
6
  "main": "dist/index.cjs",