sellmate-design-system-react 9.0.0-beta.66 → 9.0.0-beta.68

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.
Files changed (30) hide show
  1. package/AGENTS.md +125 -9
  2. package/dist/components/SBarChart/barChart.config.d.ts +0 -8
  3. package/dist/components/SCalendarBoard/CalendarDayCard.d.ts +7 -0
  4. package/dist/components/SCalendarBoard/CalendarEventButton.d.ts +3 -1
  5. package/dist/components/SCalendarBoard/CalendarMoreButton.d.ts +5 -0
  6. package/dist/components/SCalendarBoard/README.md +1 -0
  7. package/dist/components/SCalendarBoard/SCalendarBoard.d.ts +13 -1
  8. package/dist/components/SCalendarBoard/calendar-board-layout.d.ts +11 -1
  9. package/dist/components/SCalendarBoard/calendarBoard.config.d.ts +5 -0
  10. package/dist/components/SCard/README.md +1 -0
  11. package/dist/components/SCard/SCard.d.ts +10 -1
  12. package/dist/components/SFooter/README.md +16 -2
  13. package/dist/components/SFooter/SFooter.d.ts +13 -3
  14. package/dist/components/SFooter/index.d.ts +1 -1
  15. package/dist/components/SImage/README.md +3 -0
  16. package/dist/components/SImage/SImage.d.ts +9 -6
  17. package/dist/components/SLoadingContainer/index.d.ts +1 -1
  18. package/dist/components/SLoadingContainer/loading.d.ts +34 -15
  19. package/dist/components/SSectionHeaderCard/README.md +1 -0
  20. package/dist/components/SSectionHeaderCard/SSectionHeaderCard.d.ts +10 -1
  21. package/dist/index.cjs +373 -158
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.js +374 -158
  24. package/dist/index.js.map +1 -1
  25. package/dist/lib/chart/hover-dim.d.ts +19 -0
  26. package/dist/lib/hover-preview.d.ts +32 -0
  27. package/dist/llms-full.txt +147 -11
  28. package/dist/llms.txt +125 -9
  29. package/dist/styles.css +3 -0
  30. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -579,6 +579,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
579
579
  | 아이콘을 넣는다 | `SIcon` | |
580
580
  | 셀메이트 로고를 넣는다 | `SLogo` (`SIcon` 과 달리 `size` 가 **높이**이고, 색을 주지 않으면 브랜드색이다) | §3-7-14 |
581
581
  | 사진·썸네일을 보여준다 (로딩·실패 상태 포함) | `SImage` | §3-7-12 |
582
+ | 작은 썸네일을 호버해서 크게 확인하게 한다 | `SImage previewOnHover` | 툴팁·팝오버로 직접 만들지 않는다 — 미리보기는 마우스를 통과시키고 원본의 로딩·실패도 스스로 표현한다. §3-7-12 |
582
583
  | 문장 안에서 다른 화면으로 보낸다 | `STextLink` | §3-5-6 |
583
584
 
584
585
  #### C. 동작을 실행시킨다
@@ -650,8 +651,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
650
651
 
651
652
  | 하려는 일 | 컴포넌트 | 갈림 |
652
653
  | --- | --- | --- |
653
- | 화면 전체를 잠그고 기다리게 한다 | `SLoadingModal` (또는 `SModal.loading()`) | §3-2 |
654
- | 특정 영역만 덮고 기다리게 한다 | `SLoadingContainer` | §3-2 |
654
+ | 화면 전체를 잠그고 기다리게 한다 | `loading.show()` — 진행률·실패 후 재시도 버튼이 필요하면 `SLoadingModal` (`SModal.loading()`) | §3-3-6 |
655
+ | 특정 영역만 덮고 기다리게 한다 | `SLoadingContainer` | §3-3-6 |
655
656
  | 진행률을 가로 막대로 보여준다 | `SLinearProgress` | §3-7-9 |
656
657
  | 진행률·대기를 원형으로 보여준다 | `SCircleProgress` | §3-7-9 |
657
658
 
@@ -681,7 +682,7 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
681
682
  | 작업 결과를 **일시적으로** 알림 | `SToast` (+ 루트에 `SToastContainer`) |
682
683
  | 진행 전 **확인/취소**를 받아야 할 때 | `SModal.confirm({...}).onOk(...)` — 무엇을 할지가 모달 밖에서 이미 정해진 경우다 (§3-3-2) |
683
684
  | 특정 UI 요소에 대한 **온보딩·기능 안내** | `SGuide` |
684
- | 로딩 중 화면 잠금 | `SLoadingModal` / 영역 로딩은 `SLoadingContainer` |
685
+ | 로딩 중 화면 잠금 | `loading.show()` (§3-3-6) / 영역 로딩은 `SLoadingContainer` |
685
686
 
686
687
  **`SCallout` 의 `type` 은 놓이는 면이 정한다.** 안내의 강도가 아니라 배경 대비의 문제다.
687
688
 
@@ -713,7 +714,8 @@ Tailwind 유틸리티는 **토큰 스케일에 있는 값만** 사용한다.
713
714
  | --- | --- |
714
715
  | hover 시 **짧은 보조 설명** (상호작용 없음) | `STooltip` |
715
716
  | 클릭 시 **상호작용 가능한** 작은 콘텐츠 (메뉴·미니 폼) | `SPopover` |
716
- | 로딩 중 화면 잠금 | `SModal.loading(...)` / `SLoadingModal` |
717
+ | 로딩 중 화면 잠금 — 스피너와 메시지만 (기본) | `loading.show(...)` — 3-3-6 |
718
+ | 로딩 중 화면 잠금 — **진행률·실패 후 재시도 버튼**이 필요하다 | `SModal.loading(...)` / `SLoadingModal` |
717
719
  | 그 밖에 **흐름을 끊고 띄우는 창** | `SPopup` · `SActionModal` · `SModal.confirm` — 아래 3-3-1 판별 순서로 고른다 |
718
720
 
719
721
  #### 3-3-1. SPopup vs SActionModal vs SModal.confirm — 판별 순서
@@ -835,7 +837,7 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
835
837
 
836
838
  **하단 버튼을 본문(children)에 직접 두지 않는다.** 주 액션은 `button`, 보조 버튼은 `footerLeft` 로 넘긴다 — 푸터 배경·여백·양끝 분리가 컴포넌트 규칙대로 잡히는 자리다. `button` 은 클릭해도 **모달을 닫지 않으므로**(`onClick` 만 발화) 저장 API 응답을 보고 `modalRef.ok()` 로 닫으면 되고, 그 때문에 본문에 버튼을 따로 둘 이유가 없다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며 `size="md"` 를 명시한다(§3-5-2).
837
839
 
838
- **`button` 은 `SButton` 의 prop 을 그대로 받는다** — `icon`·`rightIcon`·`outline`·`disabled` 등을 함께 넘길 수 있다. 단 **`size` 는 타입에 없다**: 하단 액션 영역은 언제나 `md` 이고(§3-5-2) 예외가 없어 푸터가 고정한다. **`color` 는 주지 않는 것이 기본**이다 — 주지 않으면 주 액션 규칙대로 `primary` 가 되고, 그 버튼이 파괴적 액션일 때만 §3-5-3 에 따라 `danger` 를 준다. 카드(§3-7-8)의 `button` 도 같다.
840
+ **`button` 은 `SButton` 의 prop 을 그대로 받는다** — `icon`·`rightIcon`·`outline`·`disabled` 등을 함께 넘길 수 있다. 단 **`size` 는 타입에 없다**: 버튼 크기는 놓이는 자리가 정하므로(§3-5-2) 푸터가 준다. 모달·드로어·팝업의 푸터는 **언제나 `md`** 라 받을 것이 없다 — 크기를 고를 수 있는 것은 카드 둘뿐이다(§3-7-8 `footerSize`). **`color` 는 주지 않는 것이 기본**이다 — 주지 않으면 주 액션 규칙대로 `primary` 가 되고, 그 버튼이 파괴적 액션일 때만 §3-5-3 에 따라 `danger` 를 준다. 카드(§3-7-8)의 `button` 도 같다.
839
841
 
840
842
  **확인 모달은 확인 버튼 하나로 세우는 것이 기본이다 — `subButtonLabel` 을 주지 않는다.** 비워 두면 서브 버튼이 아예 서지 않는다. 답하지 않고 나가는 길은 **X 버튼이 이미 맡고**(늘 서 있다), `persistent` 를 켜지 않았으면 백드롭·ESC 로도 닫힌다(§3-3-5).
841
843
 
@@ -886,6 +888,78 @@ SModal.create({ component: OrderModal, componentProps: { orderId } })
886
888
 
887
889
  작성 중인 내용이 있을 때 닫기를 시도하면 이탈 안내를 띄우는 것은 **소비 앱 몫**이다. 디자인 시스템은 dirty 상태를 알지 못하므로 백드롭·ESC 를 일괄 차단할 뿐이다. 안내가 필요하면 앱이 자체 dirty 판정 후 `SModal.confirm` 으로 띄운다.
888
890
 
891
+ #### 3-3-6. 전역 로딩 오버레이 — `loading`
892
+
893
+ 화면 전체를 잠그고 기다리게 할 때 쓰는 명령형 싱글톤이다. 마운트할 것이 없다 — 첫 `show` 에 오버레이 루트가 `body` 에 자동으로 붙는다.
894
+
895
+ **표면은 둘뿐이다.**
896
+
897
+ | 하려는 일 | 사용 |
898
+ | --- | --- |
899
+ | 기다리게 한다 (거의 항상 이것) | `loading.show({ message })` → **핸들**. 부르면 걷고, `.update({ message })` 로 이 오버레이의 메시지만 바꾼다 |
900
+ | 화면 전환·에러 복구로 짝을 더는 맞출 수 없다 | `loading.hideAll()` — 비상구다. 평소에는 쓰지 않는다 |
901
+
902
+ **걷는 것은 `finally` 에 둔다.** 빠뜨리면 그 즉시 화면이 갇히고, 정상 경로에서는 아무 표시가 없다.
903
+
904
+ ```tsx
905
+ const done = loading.show({ message: '저장 중입니다...' });
906
+ try {
907
+ const payload = buildPayload(form);
908
+ done.update({ message: '전송 중입니다...' }); // 이 오버레이의 메시지만 바뀐다
909
+ await api.submit(payload);
910
+ } finally {
911
+ done(); // 이 오버레이만 걷는다
912
+ }
913
+ ```
914
+
915
+ 한 화면에서 같은 모양이 반복되면 **앱이 자기 헬퍼를 둔다.** 디자인 시스템은 원시 도구만 준다.
916
+
917
+ ```tsx
918
+ // 소비 앱 쪽 유틸 — DS 가 가지지 않는다
919
+ export const withLoading = async <T,>(task: Promise<T>, options?: { message?: string }) => {
920
+ const done = loading.show(options);
921
+ try {
922
+ return await task;
923
+ } finally {
924
+ done();
925
+ }
926
+ };
927
+ ```
928
+
929
+ - **반환 핸들을 반드시 잡는다.** 잡지 않으면 그 오버레이를 걷을 방법이 `hideAll()`(= 전부 걷기) 밖에 남지 않는다.
930
+ - **메시지를 바꾸려고 `show()` 를 다시 부르지 않는다.** 하나 더 쌓여 한 번 걷는 것으로는 닫히지 않는다. 핸들의 `update()` 를 쓴다.
931
+ - 겹쳐 부르면 그만큼 쌓이고 **마지막 하나가 걷힐 때** 닫힌다. 목록 재조회와 상세 저장이 겹쳐도 먼저 끝난 쪽이 아직 진행 중인 작업의 화면 잠금을 풀지 않는다. 화면에 그려지는 오버레이는 몇 개를 띄우든 **하나**이고, 표시되는 메시지는 맨 위 것이다.
932
+
933
+ ##### 제거된 API — 올라오면서 바꿀 것
934
+
935
+ 「스택 맨 위를 집는」 **위치 기준 API 를 전부 없앴다.** 호출자가 누구인지 볼 수 없어 겹친 작업끼리 남의 것을 걷거나 덮었고, 화면이 영영 닫히지 않는 사고의 원인이었다.
936
+
937
+ | 없어진 것 | 무엇이었나 | 무엇으로 바꾸나 |
938
+ | --- | --- | --- |
939
+ | ❌ `loading.hide()` | 맨 위 **하나**를 걷었다 | **거의 전부 `show()` 핸들**을 `finally` 에서 부르는 것으로 바뀐다. `hideAll()` 은 라우트 이탈·에러 복구처럼 **진행 중이던 남의 작업까지 정리하는 것이 의도였던** 소수의 자리에만 |
940
+ | ❌ `loading.update()` | 맨 위 항목의 메시지를 바꿨다 | `show()` 핸들의 `update()` |
941
+ | ❌ `SLoadingViewport` | 뷰포트를 직접 배치하게 했다 | 트리에서 뺀다. 루트는 첫 `show` 에 자동으로 붙으므로 직접 두면 오버레이가 **두 겹**이 되어 딤만 진해졌다 |
942
+
943
+ ⚠️ **`hideAll()` 로 일괄 치환하지 않는다.** `hide()` 가 참조 카운트 이전에는 「전부 걷기」였으므로 그쪽이 맞아 보이지만, 그 시절 호출자의 의도는 **「내가 띄운 걸 닫는다」**였다 — 큐가 있다는 것을 몰랐을 뿐이다. `hideAll()` 로 옮기면 겹친 화면에서 **아직 진행 중인 작업의 잠금까지 풀어** 재클릭·중복 요청이 난다. 기본은 핸들이다.
944
+
945
+ > **왜 이 실수가 쉬운가** — `show()` 를 몇 번 부르든 **화면에 그려지는 오버레이는 하나**다. 내부 카운트는 화면에 드러나지 않으므로 `hide()` 뒤에 오버레이가 남아도 「안 닫혔다」로만 보이고 「하나 남았다」로는 보이지 않는다. 그래서 위치로 짝을 맞추는 방식은 눈으로 검증할 수 없고, 핸들로 소유권을 명시하는 것이 유일한 방법이다.
946
+
947
+ ⚠️ **`show()` 가 두 번 불리는 경로도 함께 본다.** 같은 변경에서 `show()` 의 의미도 바뀌었다 — 그전에는 다시 부르는 것이 **갱신**이었고 지금은 하나 더 쌓인다. 그래서 아래는 `hide()` 를 안 썼어도 오버레이를 남긴다.
948
+
949
+ ```tsx
950
+ ❌ loading.show({ message: '1/2 ...' });
951
+ loading.show({ message: '2/2 ...' }); // 그때는 갱신, 지금은 하나 더 쌓인다
952
+ done(); // 하나만 걷힌다 → 남는다
953
+
954
+ ✅ const done = loading.show({ message: '1/2 ...' });
955
+ done.update({ message: '2/2 ...' });
956
+ done();
957
+ ```
958
+
959
+ > **원본(Quasar `Loading`)과의 차이는 둘뿐이고 둘 다 안전 때문이다.** 원본의 인자 없는 `hide()` 는 「전부 걷기」이므로 이름을 되쓰면 기존 호출의 의미가 컴파일 에러 없이 바뀐다 — 그래서 `hideAll()` 이다. 그리고 원본은 겹친 작업을 앱이 준 `group` 이름으로 가르지만(안 주면 먼저 끝난 쪽이 남의 잠금을 푼다), 여기서는 `show()` 마다 항목이 따로 생겨 앱이 무엇을 빠뜨려도 그 일이 없다.
960
+
961
+ > **영역만 덮을 때는 `SLoadingContainer` 를 직접 배치한다** (`open` prop 제어). `loading` 은 화면 전체를 잠그는 전역 오버레이 전용이다.
962
+
889
963
  ### 3-4. 테이블 컬럼 — 정렬·너비·헤더
890
964
 
891
965
  #### 정렬과 너비는 같은 표에서 정한다
@@ -1265,12 +1339,16 @@ tableRef.current.scrollToRow(row); // 복원
1265
1339
  | --- | --- |
1266
1340
  | 테이블 **행 내부** 인라인 액션 | `xs` |
1267
1341
  | 화면·목록 액션 (툴바, `STableBar`) | `sm` |
1268
- | **모달·위저드 푸터** | `md` |
1342
+ | **모달·드로어·팝업 푸터** | `md` (고정) |
1269
1343
  | **페이지 하단 실행 버튼** (폼 저장·상세 수정 등) | `md` |
1344
+ | **카드 푸터** — 카드가 화면의 주 액션을 쥔 경우 | `md` (`footerSize` 기본값) |
1345
+ | **카드 푸터** — 한 화면에서 부분만 확정하는 카드 | `sm` (`footerSize="sm"`) |
1270
1346
  | 스크롤 시 헤더로 승격된 버튼 | `sm` (최대) |
1271
1347
 
1272
1348
  `lg` 는 현재 사용처가 없다. 페이지 골격에서 쓰지 않는다.
1273
1349
 
1350
+ **푸터 버튼의 크기는 버튼이 아니라 푸터가 정한다.** 모달·드로어·팝업은 예외가 없어 타입에 `size` 가 없고, 카드 둘(`SCard`·`SSectionHeaderCard`)만 `footerSize` 로 고른다 — 자세한 기준은 §3-7-8. **`footerLeft` 슬롯에 직접 배치하는 `SButton` 은 그 푸터와 같은 크기를 명시한다**(모달이면 `size="md"`, `footerSize="sm"` 인 카드면 `size="sm"`). 슬롯까지는 타입이 못 막으므로 어긋나면 좌우 버튼 높이가 갈린다.
1351
+
1274
1352
  #### 3-5-3. 파괴적 액션의 채움 / outline
1275
1353
 
1276
1354
  물리적 위치가 아니라 **적용 범위 + 위험도**로 가른다.
@@ -1731,6 +1809,7 @@ useEffect(() => {
1731
1809
  - **칸 본문을 통째로 바꾸려면 `dayContent` 다.** 일정으로 표현되지 않는 그날치 요약(재고·매출 같은)을 칸에 얹을 때 쓴다. **슬롯을 준 날짜는 그 날짜의 `events` 를 그리지 않고 목록 판도 열지 않는다** — 한 칸에 둘을 겹쳐 담지 않는다. 일정과 요약을 함께 보여야 하면 요약을 일정 한 건(`events[]`)으로 넣는다.
1732
1810
  - **`SCalendarBoard` 는 높이를 부모에게서 받는다.** 행이 그 높이를 균등하게 나눠 갖고, 칸 높이가 다시 "한 칸에 몇 줄이 보일지" 를 정한다. 그래서 **놓는 자리에 높이를 준다** — 페이지 본문의 남은 높이를 채우려면 `SPage` 안에서 스택을 `flex-1 min-h-0` 으로 두고 판에 `h-full` 을 준다. 높이를 주지 않으면 칸이 최소 높이로 주저앉아 한두 줄만 보인다.
1733
1811
  - **한 칸에 몇 줄을 펼칠지는 컴포넌트가 잰다.** 조절하는 prop 이 없다 — 칸 높이에서 나오는 값이라 따로 정할 것이 아니다. 줄이 넘치면 마지막 한 줄을 "N개 더보기" 에 내주므로 **접히든 아니든 칸 높이가 같다.** 기간 막대가 접히면 **걸친 날마다 한 건으로** 세므로 날짜별 "N개" 가 서로 다를 수 있다. **소비 앱이 `events` 를 미리 잘라 넘기지 않는다.** 잘라 넘기면 "N개 더보기" 의 수가 실제 건수와 어긋나고, 눌러도 나머지가 나오지 않는다.
1812
+ - **판 밖의 것에 손이 닿았을 때 그에 맞는 일정을 짚어 주려면 `focusedEventId` 다.** 지목한 일정만 또렷하게 남고 나머지 일정과 "N개 더보기" 는 물러난다. **지목한 일정이 "N개 더보기" 에 접혀 있으면 그 더보기 자리에 대신 그려져** 지목하는 동안만 드러나고, 보이던 일정들의 줄 배정은 어느 쪽이든 달라지지 않는다 — 지목이 오가도 판이 출렁이지 않는다. 자리를 내준 칸은 그동안 "더보기" 를 그리지 않으므로, 거기 남은 일정은 칸을 눌러 목록 판으로 본다. **물러나게 하는 것을 앱에서 직접 하지 않는다** — 접혀 있다 드러난 막대는 앱의 CSS 가 닿지 않아 그 하나만 어긋난다. `events[].id` 가 없는 일정은 지목할 수 없다. 호버·포커스를 **감지**하는 것은 앱 몫이고, 판은 "지금 어느 일정인가" 만 받는다.
1734
1813
  - **`events[].label` 은 필수다** — 일정이 무엇인지 알리는 부분이라 비워 두지 않는다. 인라인 HTML 을 받으므로 부분 강조가 필요할 때만 태그를 쓰고, 나머지는 `description`·`value` 로 나눠 담는다. 칸이 좁아지면 `label`+`description` 이 한 덩어리로 말줄임되고 `value` 만 끝에 온전히 남는다 — **금액·수량은 반드시 `value` 에 담는다.** `description` 에 넣으면 잘린다.
1735
1814
  - `SDatePickerYearListbox`·`SDatePickerMonthListbox` 는 `SDatePicker` 의 mode listbox 조각만 떼어낸 컴포넌트다. 일반 폼 입력에는 `SDatePicker mode="year" | "month"` 를 우선 쓰고, 다른 트리거·팝오버 안에 리스트만 끼워 넣을 때만 직접 쓴다.
1736
1815
 
@@ -1855,7 +1934,7 @@ const [selectedId, setSelectedId] = useState<string>();
1855
1934
  - **카드 안에 카드를 겹치지 않는다.** 섹션 안을 더 나눠야 하면 `SDivider` 로 끊거나(§3-6) 섹션을 둘로 분리한다.
1856
1935
  - 안쪽 여백은 `SSectionHeaderCard` 의 `padding` prop 으로 준다 — `p-sd-*` 를 직접 주지 않는다 (§2-2).
1857
1936
 
1858
- **카드가 자기 안에서 확정하는 액션을 가지면 하단 버튼을 `children` 에 직접 두지 않는다.** 두 카드 모두 모달·드로어와 같은 하단 액션 영역을 갖는다 — 주 액션은 `button`, 보조 버튼은 `footerLeft` 로 넘긴다(§3-3-4 와 같은 규칙). 배경·상단 구분선·좌우 여백·양끝 분리가 컴포넌트 규칙대로 잡히고, 좌우 끝이 헤더·본문과 맞는다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며 `size="md"` 를 명시한다(§3-5-2).
1937
+ **카드가 자기 안에서 확정하는 액션을 가지면 하단 버튼을 `children` 에 직접 두지 않는다.** 두 카드 모두 모달·드로어와 같은 하단 액션 영역을 갖는다 — 주 액션은 `button`, 보조 버튼은 `footerLeft` 로 넘긴다(§3-3-4 와 같은 규칙). 배경·상단 구분선·좌우 여백·양끝 분리가 컴포넌트 규칙대로 잡히고, 좌우 끝이 헤더·본문과 맞는다. `footerLeft` 는 슬롯이라 `SButton` 을 직접 배치하며 푸터와 같은 크기를 명시한다(§3-5-2).
1859
1938
 
1860
1939
  ```tsx
1861
1940
  {/* 섹션 안에서 바로 수정·저장하는 인라인 폼 섹션 (§4-4) */}
@@ -1869,6 +1948,30 @@ const [selectedId, setSelectedId] = useState<string>();
1869
1948
  </SSectionHeaderCard>
1870
1949
  ```
1871
1950
 
1951
+ **카드 푸터만 크기를 고른다 — `footerSize`.** 모달·드로어·팝업의 푸터는 `md` 고정이지만(§3-5-2), 카드는 화면에서 차지하는 자리가 둘로 갈린다.
1952
+
1953
+ | 카드가 놓인 자리 | `footerSize` |
1954
+ | --- | --- |
1955
+ | 카드가 **화면의 주 액션**을 쥔다 (한 폭을 차지하는 상세·폼 섹션) | `md` — 기본값이라 주지 않는다 |
1956
+ | 한 화면에서 **부분만 확정한다** (옆 패널·분할 영역처럼 화면의 일부를 맡는 카드) | `sm` |
1957
+
1958
+ - **`sm` 은 카드에만 있다.** 그 카드가 모달·팝업 안에 들어 있어도 같다 — 모달 자신의 푸터는 여전히 `md` 다.
1959
+ - **`footerSize="sm"` 이면 `footerLeft` 슬롯 버튼도 `size="sm"` 으로 맞춘다.** 슬롯은 타입이 못 막아서, 한쪽만 `md` 로 남으면 좌우 버튼 높이가 갈린 푸터가 된다.
1960
+ - 여백은 `md`·`sm` 이 같다 — 푸터 패딩 토큰이 한 벌이라 **바뀌는 것은 버튼 크기뿐**이다. 여백을 직접 주지 않는다.
1961
+
1962
+ ```tsx
1963
+ {/* 화면 오른쪽 패널만 확정하는 카드 — 좌우 버튼 크기를 footerSize 에 맞춘다 */}
1964
+ <SSectionHeaderCard
1965
+ title="옵션 템플릿"
1966
+ marker
1967
+ footerSize="sm"
1968
+ footerLeft={<SButton color="danger" outline size="sm" icon="remove" label="템플릿 삭제" onClick={remove} />}
1969
+ button={{ label: '저장', onClick: save }}
1970
+ >
1971
+ …
1972
+ </SSectionHeaderCard>
1973
+ ```
1974
+
1872
1975
  **페이지 전체를 확정하는 액션은 카드 푸터가 아니라 페이지 하단에 둔다.** 카드 푸터는 **그 카드 안에서 닫히는 액션**의 자리다 — 여러 섹션을 한 번에 저장하는 버튼을 마지막 카드의 푸터에 넣으면 그 카드에만 걸리는 액션으로 읽힌다. 이때는 §4-4 처럼 카드 밖 하단 줄에 둔다.
1873
1976
 
1874
1977
  #### 3-7-9. SLinearProgress vs SCircleProgress
@@ -1910,13 +2013,23 @@ const [selectedId, setSelectedId] = useState<string>();
1910
2013
  - **크기는 `ratio` 와 너비로 준다.** `ratio` 기본값이 `1`(정사각형)이라 너비만 주면 정사각형이 된다. `className`/`style` 로 높이를 직접 주면 그쪽이 이기고 `ratio` 는 무시된다.
1911
2014
  - 상자를 채우는 방식은 `fit`(기본 `cover`), 붙는 위치는 `position` 이다. 안쪽 `<img>` 에 네이티브 속성이 필요하면 `imgProps` 로 넘긴다 — 목록에서 고스트 드래그를 막는 `imgProps={{ draggable: false }}` 가 대표적이다.
1912
2015
  - 썸네일이 많은 목록에서는 `loadingShowDelay` 를 준다. 캐시된 이미지가 즉시 로드될 때 스피너가 한 프레임 번쩍이는 것을 막는다.
2016
+ - **썸네일이 작아 내용을 알아볼 수 없으면 `previewOnHover` 를 켠다.** 호버하면 큰 이미지가 포털로 뜬다 — 상품 목록에서 사진을 확인하려고 상세 화면까지 들어가는 일을 없앤다. **툴팁이나 팝오버로 직접 만들지 않는다**: 미리보기는 마우스를 통과시켜 아래 행의 클릭을 가로채지 않고, 원본의 로딩·실패도 스스로 표현한다.
2017
+ - 목록 썸네일과 원본 URL 이 다르면 원본을 `previewSrc` 로 준다. 주지 않으면 썸네일을 그대로 확대해서 흐릿하다.
2018
+ - **뜨는 자리를 앱이 정하지 않는다.** 미리보기는 썸네일 한가운데에서 오른쪽 아래로 펼쳐지고, 창 밖으로 넘치면 넘친 만큼만 왼쪽·위로 밀린다(반대편으로 뒤집히지 않는다). 방향을 고르는 prop 은 없다.
2019
+ - 크기는 `previewSize` 로 줄일 수 있고 **상한은 200px** 이다. 그보다 큰 값을 줘도 200 으로 잘린다 — 미리보기가 화면을 덮지 않게 하는 규격이다.
2020
+ - 미리보기 상자는 정사각형이고 이미지는 `contain` 으로 들어간다 — 세로로 긴 이미지도 잘리지 않는다. `ratio`·`fit` 은 썸네일 쪽에만 적용된다.
1913
2021
 
1914
2022
  ```tsx
1915
2023
  ✅ <SImage src={item.thumbnailUrl} alt={item.name} style={{ width: 64 }} /> {/* 정사각 썸네일 */}
1916
2024
  ✅ <SImage src={banner} ratio={16 / 9} className="w-full" /> {/* 가로형 배너 */}
2025
+ ✅ <SImage src={item.thumbnailUrl} previewOnHover previewSrc={item.imageUrl} {/* 호버로 원본 확인 */}
2026
+ alt={item.name} style={{ width: 64 }} />
1917
2027
 
1918
2028
  ❌ <img src={item.thumbnailUrl} /> {/* 로딩·실패 상태가 없다 */}
1919
2029
  ❌ {loading ? <SCircleProgress indeterminate /> : <SImage src={url} />} {/* SImage 가 이미 한다 */}
2030
+ ❌ <STooltip content={<img src={item.imageUrl} />}> {/* previewOnHover 를 쓴다 */}
2031
+ <SImage src={item.thumbnailUrl} />
2032
+ </STooltip>
1920
2033
  ```
1921
2034
 
1922
2035
  #### 3-7-14. 로고 — SLogo
@@ -2785,9 +2898,10 @@ export default function ProductDetailPage() {
2785
2898
  | Prop (Footer) | 용도 |
2786
2899
  | --- | --- |
2787
2900
  | `button` | 하단 액션 영역 우측 주 액션 (§3-7-8) |
2788
- | `footerLeft` | 하단 액션 영역 좌측 슬롯 — 보조 버튼. `SButton` 에 `size="md"` 를 명시한다 |
2901
+ | `footerLeft` | 하단 액션 영역 좌측 슬롯 — 보조 버튼. `SButton` 에 `footerSize` 와 같은 크기를 명시한다 |
2902
+ | `footerSize` | 하단 액션 영역 크기 — `'md'`(기본) / `'sm'`. 판정은 §3-7-8 |
2789
2903
 
2790
- 둘 중 하나라도 주면 하단 액션 영역이 렌더된다. 회색 바탕 + 상단 구분선이며 좌우 끝은 헤더에 맞는다 — 배경·여백을 직접 주지 않는다.
2904
+ `button`·`footerLeft` 중 하나라도 주면 하단 액션 영역이 렌더된다. 회색 바탕 + 상단 구분선이며 좌우 끝은 헤더에 맞는다 — 배경·여백을 직접 주지 않는다.
2791
2905
 
2792
2906
  **한 페이지에 섹션 카드가 둘 이상이면 `marker` 를 켠다.** 점은 섹션을 서로 구분할 대상이 여럿일 때만 의미가 있어, 카드가 하나뿐인 페이지에서는 켜지 않는다. 한 페이지 안에서는 켜거나 끄거나 전부 같게 간다.
2793
2907
 
@@ -3102,6 +3216,7 @@ export default function ExcelFilePopupPage() {
3102
3216
  - [ ] 하단 버튼이 양끝 분리(`justify-between`)이고, 되돌리기가 왼쪽 끝 · 실행이 오른쪽 끝인가 (되돌리는 액션이 없어도 양끝 분리는 그대로 — 오른쪽에 모으지 않고 위계가 낮은 쪽을 왼쪽 끝에 둔다)
3103
3217
  - [ ] 페이지에 `color="primary"` 채움 버튼이 **1개뿐**인가 (`danger` 채움도 1개, `SDropdownButton` 포함)
3104
3218
  - [ ] 버튼 `size` 가 위치 규칙과 맞는가 (행 내부 `xs` / 화면 액션 `sm` / 모달 푸터 `md`)
3219
+ - [ ] 카드 `footerSize` 가 카드가 놓인 자리와 맞고(화면의 주 액션 `md` / 부분만 확정 `sm`, §3-7-8), `footerLeft` 슬롯 버튼의 `size` 를 그 값에 맞췄는가
3105
3220
  - [ ] 파괴 액션의 채움/outline 이 **적용 범위 + 위험도** 기준과 맞는가
3106
3221
  - [ ] `SGhostButton` 이 아이콘만으로 뜻이 통하는 부가 위치(표·모달 헤더·카드)에만 쓰였고, `ariaLabel` 이 전부 있는가 (라벨이 필요하면 `SButton`)
3107
3222
  - [ ] `SGhostButton` 의 `intent` 가 조작 성격과 맞는가 (되돌릴 수 없는 삭제만 `danger`, 진입·추가는 `action`, 나머지는 `default`)
@@ -3110,6 +3225,7 @@ export default function ExcelFilePopupPage() {
3110
3225
  - [ ] 모달·드로어의 하단 버튼을 본문이 아니라 `button` · `footerLeft` prop 으로 넘겼는가 (§3-3-4), 카드 안에서 닫히는 액션도 같은 prop 으로 넘겼는가 (§3-7-8)
3111
3226
  - [ ] 아래에 버튼이 있는 자리(모달·드로어·카드)의 `STextarea autogrow` 에 `maxRows` 를 함께 줬는가 (§3-7-1 — 없으면 긴 글이 버튼을 화면 밖으로 밀어낸다)
3112
3227
  - [ ] 앱 부트스트랩의 Provider 안쪽에 `<SModalOutlet />` 이 한 번 렌더되어 있는가 (§4-1 — 없으면 모달 안에서 앱 훅이 죽는다), 그 대신으로 모달 컴포넌트를 Provider 로 다시 감싸지 않았는가
3228
+ - [ ] 전역 로딩의 `loading.show()` 반환 핸들을 잡아 `finally` 에서 그것만 불렀는가, 메시지를 바꾸려고 `show()` 를 두 번 부르지는 않았는가 (§3-3-6 — 걷기를 빠뜨리면 화면이 갇히고, 두 번 부르면 하나 더 쌓여 오버레이가 남는다)
3113
3229
  - [ ] 고른 컴포넌트를 §2-0 의 제 층에 놓았는가 (요소를 `SPage` 에 직접 놓지 않았는가, 블록을 `div` 로 감싸지 않았는가)
3114
3230
  - [ ] §2-0 포함 규칙을 지켰는가 (카드 안 카드 ❌, 표 셀 안 블록 ❌)
3115
3231
  - [ ] 블록 순서가 §2-0 순서와 맞는가 (제목 → 안내 → 필터 → 툴바 → 본문 → 페이지네이션 → 하단 액션)
@@ -51,14 +51,6 @@ export declare const BAR_DATA_LABEL_COLOR = "var(--cmp-chart-dataLabel-color-def
51
51
  */
52
52
  export declare const BAR_DATA_LABEL_TYPO = "typo-body-sm-medium";
53
53
  export declare const BAR_DATA_LABEL_TYPO_HOVER = "typo-body-md-bold";
54
- /**
55
- * 흐려지고 또렷해지는 전환 — 막대와 값 라벨에 같이 건다.
56
- *
57
- * 둘의 속도가 다르면 막대는 이미 물러났는데 숫자만 뒤늦게 따라와 어긋나 보인다.
58
- * 전환 대상은 불투명도뿐이다. 짚은 항목의 라벨이 커지는 것(`typo-*` 교체)은
59
- * 글자가 늘어나는 중간 상태가 오히려 읽기 나빠 즉시 바꾼다.
60
- */
61
- export declare const BAR_HOVER_TRANSITION = "transition-opacity duration-200 ease-out motion-reduce:transition-none";
62
54
  export interface BandLayout {
63
55
  /** 카테고리 하나가 차지하는 폭(px) */
64
56
  band: number;
@@ -36,6 +36,13 @@ export declare const CalendarDayCard: import("react").ForwardRefExoticComponent<
36
36
  hiddenCount: number;
37
37
  /** "더보기" 가 앉는 줄. 접힌 것이 없는 주는 -1 */
38
38
  moreLane: number;
39
+ /** "더보기" 자리를 집중된 일정에 내줬는지 — 내준 칸은 "더보기" 를 그리지 않는다 */
40
+ moreReplaced: boolean;
41
+ /**
42
+ * 집중할 일정의 `id` (SCalendarBoard 의 `focusedEventId`).
43
+ * 주면 그 일정만 또렷하게 남고 이 칸의 나머지 일정과 "더보기" 는 흐려진다.
44
+ */
45
+ focusedEventId?: string | null;
39
46
  /** 이 칸의 일정 목록 판이 열려 있는지 */
40
47
  isOpen: boolean;
41
48
  moreLabel?: string;
@@ -6,13 +6,15 @@ import type { SCalendarBoardEvent } from './types';
6
6
  * 점 → 레이블(강조) → 설명 → `/` → 값 순서로 한 줄에 눕는다.
7
7
  * 가운데 레이블 묶음만 줄어들고 값은 끝에 고정되므로, 좁은 칸에서도 금액이 잘리지 않는다.
8
8
  */
9
- export declare function CalendarEventButton({ event, onSelect, flatStart, flatEnd, className, style, }: {
9
+ export declare function CalendarEventButton({ event, onSelect, flatStart, flatEnd, dimmed, className, style, }: {
10
10
  event: SCalendarBoardEvent;
11
11
  onSelect?: (event: SCalendarBoardEvent, e: MouseEvent<HTMLButtonElement>) => void;
12
12
  /** 왼쪽 끝이 잘린 조각 — 앞 주에서 이어지므로 모서리를 평평하게 한다 */
13
13
  flatStart?: boolean;
14
14
  /** 오른쪽 끝이 잘린 조각 — 다음 주로 이어진다 */
15
15
  flatEnd?: boolean;
16
+ /** 다른 일정에 집중하는 동안 물러나 있다 — 흐려질 뿐 계속 눌린다 */
17
+ dimmed?: boolean;
16
18
  className?: string;
17
19
  style?: CSSProperties;
18
20
  }): import("react").JSX.Element;
@@ -7,6 +7,11 @@ export declare const CalendarMoreButton: import("react").ForwardRefExoticCompone
7
7
  count: number;
8
8
  /** 개수 뒤에 붙는 말 */
9
9
  label?: string;
10
+ /**
11
+ * 어느 일정에 집중하는 동안 물러나 있다. 여기 남아 있다는 것은 접힌 것 중에
12
+ * 집중 대상이 없다는 뜻이라(있었다면 이 자리를 내줬다) 함께 흐려진다.
13
+ */
14
+ dimmed?: boolean;
10
15
  onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
11
16
  className?: string;
12
17
  style?: CSSProperties;
@@ -17,6 +17,7 @@
17
17
  | `events?` | `SCalendarBoardEvent[]` | — | 일정 목록. `date`(시작)로 칸에 배분되고, `endDate` 를 주면 그날까지 **하나의 막대로 이어져** 칸 경계를 넘어 그려진다. |
18
18
  | `dayColors?` | `Record<string, SCalendarBoardDayColor>` | — | 날짜별 칸 배경. 키는 `YYYY-MM-DD`, 없는 날짜는 `'default'` 다. 그날의 성격(마감·완료·휴무 등)을 칸 전체로 알릴 때 쓴다 — 일정 하나의 색은 `events[].color` 다. |
19
19
  | `dayContent?` | `Record<string, ReactNode>` | — | 날짜별 칸 본문. 키는 `YYYY-MM-DD`, 준 날짜는 **일정 자리를 통째로 이 내용이 차지한다** — 그날의 `events` 는 그리지 않고 목록 판도 열리지 않는다. 일정으로 표현되지 않는 것(재고 현황· 매출 요약 같은 그날치 요약)을 칸에 그대로 얹을 때 쓴다. |
20
+ | `focusedEventId?` | `string \| null` | — | 집중할 일정의 `id`. 그 일정만 또렷하게 남고 **나머지 일정과 "N개 더보기" 는 흐려진다** — 판 밖의 무언가(목록의 행 등)에 손이 닿았을 때 판에서 그에 대응하는 일정을 짚어 주는 데 쓴다. 그 일정이 칸에 담기는 줄 수를 넘겨 **"N개 더보기" 에 접혀 있으면, 그 "더보기" 자리에 대신 그려져** 집중하는 동안만 드러난다. 이미 보이고 있으면 자리는 그대로다 — 어느 쪽이든 다른 일정의 줄 배정은 달라지지 않으므로, 집중이 오가도 판이 출렁이지 않는다. 자리를 내준 칸은 그 동안 "더보기" 를 그리지 않는다(한 줄에 둘을 넣을 수 없다). 거기 남은 일정은 칸을 눌러 목록 판으로 본다. `id` 가 없는 일정은 지목할 수 없다. |
20
21
  | `todayLabel?` | `string` | `'오늘'` | 헤더 오른쪽 "오늘" 버튼 레이블. 빈 문자열이면 버튼을 그리지 않는다 |
21
22
  | `moreLabel?` | `string` | — | "N개" 뒤에 붙는 말 |
22
23
  | `headerRight?` | `ReactNode` | — | 헤더 오른쪽 끝에 붙일 추가 컨트롤 |
@@ -38,6 +38,18 @@ export interface SCalendarBoardProps {
38
38
  * 매출 요약 같은 그날치 요약)을 칸에 그대로 얹을 때 쓴다.
39
39
  */
40
40
  dayContent?: Record<string, ReactNode>;
41
+ /**
42
+ * 집중할 일정의 `id`. 그 일정만 또렷하게 남고 **나머지 일정과 "N개 더보기" 는 흐려진다** —
43
+ * 판 밖의 무언가(목록의 행 등)에 손이 닿았을 때 판에서 그에 대응하는 일정을 짚어 주는 데 쓴다.
44
+ *
45
+ * 그 일정이 칸에 담기는 줄 수를 넘겨 **"N개 더보기" 에 접혀 있으면, 그 "더보기" 자리에 대신
46
+ * 그려져** 집중하는 동안만 드러난다. 이미 보이고 있으면 자리는 그대로다 — 어느 쪽이든 다른
47
+ * 일정의 줄 배정은 달라지지 않으므로, 집중이 오가도 판이 출렁이지 않는다.
48
+ *
49
+ * 자리를 내준 칸은 그 동안 "더보기" 를 그리지 않는다(한 줄에 둘을 넣을 수 없다). 거기 남은
50
+ * 일정은 칸을 눌러 목록 판으로 본다. `id` 가 없는 일정은 지목할 수 없다.
51
+ */
52
+ focusedEventId?: string | null;
41
53
  /** 일정 클릭 */
42
54
  onEventClick?: (event: SCalendarBoardEvent, e: MouseEvent<HTMLButtonElement>) => void;
43
55
  /**
@@ -68,4 +80,4 @@ export interface SCalendarBoardProps {
68
80
  * 보일지" 를 정한다 — 그래서 놓는 자리에 높이를 주어야 한다 (`<div className="h-[640px]">` ·
69
81
  * `SPage` 본문의 남은 높이 등). 높이를 주지 않으면 칸이 `dayCard.minHeight` 로 주저앉는다.
70
82
  */
71
- export declare function SCalendarBoard({ year, month, defaultYear, defaultMonth, onViewChange, selectable, value, onValueChange, events, dayColors, dayContent, onEventClick, onMoreClick, todayLabel, moreLabel, headerRight, className, style, }: SCalendarBoardProps): import("react").JSX.Element;
83
+ export declare function SCalendarBoard({ year, month, defaultYear, defaultMonth, onViewChange, selectable, value, onValueChange, events, dayColors, dayContent, focusedEventId, onEventClick, onMoreClick, todayLabel, moreLabel, headerRight, className, style, }: SCalendarBoardProps): import("react").JSX.Element;
@@ -36,14 +36,24 @@ export interface CalendarBoardLayout {
36
36
  hiddenCountByCell: number[];
37
37
  /** 주별로 "N개 더보기" 가 앉는 줄. 접힌 것이 없는 주는 -1 */
38
38
  moreLaneByWeek: number[];
39
+ /**
40
+ * 칸별로, "더보기" 자리를 집중된 일정(`focusedEventId`)에 내줬는지.
41
+ * 내준 칸은 그 줄에 막대가 앉았으므로 "더보기" 를 그리지 않는다 — 한 줄에 둘을 넣을 수 없다.
42
+ */
43
+ moreReplacedByCell: boolean[];
39
44
  }
40
45
  /** 일정의 마지막 날. `endDate` 가 없거나 시작보다 앞서면 하루짜리로 본다 */
41
46
  export declare function eventEndDate(event: SCalendarBoardEvent): string;
42
- export declare function layoutCalendarBoard({ cells, events, rowCapacity, blockedDates, }: {
47
+ export declare function layoutCalendarBoard({ cells, events, rowCapacity, blockedDates, focusedEventId, }: {
43
48
  cells: readonly DayCell[];
44
49
  events: readonly SCalendarBoardEvent[];
45
50
  /** 칸 하나에 담기는 줄 수 */
46
51
  rowCapacity: number;
47
52
  /** 일정을 그리지 않는 날짜 (`dayContent` 를 받은 칸) */
48
53
  blockedDates: ReadonlySet<string>;
54
+ /**
55
+ * 집중할 일정의 `id`. 그 일정이 접혀 있으면 "더보기" 가 예약한 줄을 대신 차지해 노출된다 —
56
+ * 이미 보이고 있으면 아무것도 달라지지 않는다. 줄 배정 자체는 이 값과 무관하다.
57
+ */
58
+ focusedEventId?: string | null;
49
59
  }): CalendarBoardLayout;
@@ -23,6 +23,11 @@ export declare const CALENDAR_BOARD_EVENT_PRESETS: Record<SCalendarBoardEventCol
23
23
  * 그래서 이 값을 조절하는 prop 은 없다.
24
24
  */
25
25
  export declare const CALENDAR_BOARD_MAX_VISIBLE_EVENTS = 4;
26
+ /**
27
+ * 집중하지 않은 것(다른 일정과 "N개 더보기")이 물러나는 투명도.
28
+ * 대응 토큰이 없어 디자인 값을 그대로 쓴다 — 토큰이 생기면 여기만 갈아끼운다.
29
+ */
30
+ export declare const CALENDAR_BOARD_DIM_OPACITY = 0.4;
26
31
  /** 날짜 칸 배경 — 그날의 성격을 칸 전체로 알린다 */
27
32
  export declare const CALENDAR_BOARD_DAY_COLORS: readonly ["default", "danger", "accent", "complete", "dim"];
28
33
  export type SCalendarBoardDayColor = (typeof CALENDAR_BOARD_DAY_COLORS)[number];
@@ -11,6 +11,7 @@
11
11
  | `bordered?` | `boolean` | `false` | 테두리 표시 여부 |
12
12
  | `footerLeft?` | `ReactNode` | — | 하단 액션 영역 좌측 슬롯. 보조 버튼(취소·목록 등)이 오는 자리다. `footerLeft` 또는 `button` 중 하나라도 있으면 하단 액션 영역이 렌더된다. |
13
13
  | `button?` | `SFooterButton` | — | 하단 액션 영역 우측 주 액션 버튼 |
14
+ | `footerSize?` | `SFooterSize` | `'md'` | 하단 액션 영역 크기. 기본은 `md`. 한 화면 안에서 **부분만 확정하는 카드**(옆 패널·분할 영역처럼 화면의 일부를 맡는 카드)일 때만 `sm`. 카드가 화면의 주 액션을 쥐고 있으면 모달·페이지와 같은 `md` 다 (AGENTS.md §3-5-2). `footerLeft` 슬롯의 버튼도 같이 맞춘다 — `sm` 이면 슬롯 버튼도 `size="sm"`. |
14
15
  | `footerClassName?` | `string` | — | 하단 액션 영역 클래스 |
15
16
 
16
17
  ## Dependencies
@@ -1,5 +1,5 @@
1
1
  import { type HTMLAttributes, type ReactNode } from 'react';
2
- import { type SFooterButton } from '../SFooter';
2
+ import { type SFooterButton, type SFooterSize } from '../SFooter';
3
3
  export interface SCardProps extends HTMLAttributes<HTMLDivElement> {
4
4
  /** 테두리 표시 여부 */
5
5
  bordered?: boolean;
@@ -10,6 +10,15 @@ export interface SCardProps extends HTMLAttributes<HTMLDivElement> {
10
10
  footerLeft?: ReactNode;
11
11
  /** 하단 액션 영역 우측 주 액션 버튼 */
12
12
  button?: SFooterButton;
13
+ /**
14
+ * 하단 액션 영역 크기. 기본은 `md`.
15
+ *
16
+ * 한 화면 안에서 **부분만 확정하는 카드**(옆 패널·분할 영역처럼 화면의 일부를 맡는 카드)일 때만
17
+ * `sm`. 카드가 화면의 주 액션을 쥐고 있으면 모달·페이지와 같은 `md` 다 (AGENTS.md §3-5-2).
18
+ *
19
+ * `footerLeft` 슬롯의 버튼도 같이 맞춘다 — `sm` 이면 슬롯 버튼도 `size="sm"`.
20
+ */
21
+ footerSize?: SFooterSize;
13
22
  /** 하단 액션 영역 클래스 */
14
23
  footerClassName?: string;
15
24
  }
@@ -9,6 +9,7 @@
9
9
  | Prop | Type | Default | Description |
10
10
  |------|------|---------|-------------|
11
11
  | `bg?` | `SFooterBg` | `'white'` | 배경색 |
12
+ | `size?` | `SFooterSize` | `'md'` | 크기. 기본은 `md` — 오버레이 하단 액션 영역의 크기다 |
12
13
  | `left?` | `ReactNode` | — | footer 좌측 slot |
13
14
  | `children?` | `ReactNode` | — | footer 좌측 slot. left와 함께 쓰면 left 뒤에 렌더된다 |
14
15
  | `button?` | `SFooterButton` | — | 우측 기본 액션 버튼 |
@@ -24,6 +25,19 @@
24
25
  export type SFooterBg = 'white' | 'grey';
25
26
  ```
26
27
 
28
+ ### SFooterSize
29
+
30
+ ```ts
31
+ /**
32
+ * footer 크기. 버튼 크기가 여기에 딸려 온다 — `md` 는 `SButton size="md"`, `sm` 은 `"sm"`.
33
+ *
34
+ * 버튼 크기를 따로 받지 않고 footer 자체를 분기하는 이유: 크기를 정하는 것은 버튼이 아니라
35
+ * **버튼이 놓인 자리**이기 때문이다 (AGENTS.md §3-5-2). 좌측 슬롯 버튼까지 한 벌로 맞춰야
36
+ * 하는데, 우측 버튼만 크기를 받으면 좌우가 어긋난 footer 를 만들 길이 열린다.
37
+ */
38
+ export type SFooterSize = 'sm' | 'md';
39
+ ```
40
+
27
41
  ### SFooterButton
28
42
 
29
43
  ```ts
@@ -33,8 +47,8 @@ export type SFooterBg = 'white' | 'grey';
33
47
  * 골라 담은 부분집합으로 두지 않는 이유: `SButton` 에 prop 이 늘 때마다 이 목록만 조용히
34
48
  * 뒤처져, 아이콘 하나 붙이려 해도 footer 에서는 길이 없는 상태가 된다.
35
49
  *
36
- * **`size` 만 뺀다.** 버튼 크기는 놓이는 위치가 정하고 하단 액션 영역은 언제나 `md` 다
37
- * (AGENTS.md §3-5-2 — 예외가 없는 규칙이라 문서가 아니라 타입이 지킨다).
50
+ * **`size` 만 뺀다.** 버튼 크기는 놓이는 위치가 정하므로 footer 의 `size` 가 정한다
51
+ * (AGENTS.md §3-5-2) — 버튼이 자기 크기를 따로 주장할 자리가 아니다.
38
52
  *
39
53
  * `label`·`color` 는 주지 않으면 footer 가 주 액션 기본값(`'확인'`·`'primary'`)으로 잡는다.
40
54
  * 파괴적 액션이라 `danger` 가 필요한 경우는 열어 둔다 (§3-5-3).
@@ -1,14 +1,22 @@
1
1
  import { type CSSProperties, type ReactNode } from 'react';
2
2
  import { type SButtonProps } from '../SButton';
3
3
  export type SFooterBg = 'white' | 'grey';
4
+ /**
5
+ * footer 크기. 버튼 크기가 여기에 딸려 온다 — `md` 는 `SButton size="md"`, `sm` 은 `"sm"`.
6
+ *
7
+ * 버튼 크기를 따로 받지 않고 footer 자체를 분기하는 이유: 크기를 정하는 것은 버튼이 아니라
8
+ * **버튼이 놓인 자리**이기 때문이다 (AGENTS.md §3-5-2). 좌측 슬롯 버튼까지 한 벌로 맞춰야
9
+ * 하는데, 우측 버튼만 크기를 받으면 좌우가 어긋난 footer 를 만들 길이 열린다.
10
+ */
11
+ export type SFooterSize = 'sm' | 'md';
4
12
  /**
5
13
  * footer 우측 기본 액션 버튼 설정 — `SButton` 의 prop 을 그대로 받는다.
6
14
  *
7
15
  * 골라 담은 부분집합으로 두지 않는 이유: `SButton` 에 prop 이 늘 때마다 이 목록만 조용히
8
16
  * 뒤처져, 아이콘 하나 붙이려 해도 footer 에서는 길이 없는 상태가 된다.
9
17
  *
10
- * **`size` 만 뺀다.** 버튼 크기는 놓이는 위치가 정하고 하단 액션 영역은 언제나 `md` 다
11
- * (AGENTS.md §3-5-2 — 예외가 없는 규칙이라 문서가 아니라 타입이 지킨다).
18
+ * **`size` 만 뺀다.** 버튼 크기는 놓이는 위치가 정하므로 footer 의 `size` 가 정한다
19
+ * (AGENTS.md §3-5-2) — 버튼이 자기 크기를 따로 주장할 자리가 아니다.
12
20
  *
13
21
  * `label`·`color` 는 주지 않으면 footer 가 주 액션 기본값(`'확인'`·`'primary'`)으로 잡는다.
14
22
  * 파괴적 액션이라 `danger` 가 필요한 경우는 열어 둔다 (§3-5-3).
@@ -21,6 +29,8 @@ export type SFooterButton = Omit<SButtonProps, 'size'>;
21
29
  export interface SFooterProps {
22
30
  /** 배경색 */
23
31
  bg?: SFooterBg;
32
+ /** 크기. 기본은 `md` — 오버레이 하단 액션 영역의 크기다 */
33
+ size?: SFooterSize;
24
34
  /** footer 좌측 slot */
25
35
  left?: ReactNode;
26
36
  /** footer 좌측 slot. left와 함께 쓰면 left 뒤에 렌더된다 */
@@ -39,4 +49,4 @@ export interface SFooterProps {
39
49
  * 공통화한다. 소비자는 호스트 컴포넌트의 `footerLeft`·`button` prop 으로만 닿는다 —
40
50
  * 패키지 public export 에 추가하지 말 것.
41
51
  */
42
- export declare function SFooter({ bg, left, button, children, className, leftClassName, style, }: SFooterProps): import("react").JSX.Element;
52
+ export declare function SFooter({ bg, size, left, button, children, className, leftClassName, style, }: SFooterProps): import("react").JSX.Element;
@@ -1 +1 @@
1
- export { SFooter, type SFooterBg, type SFooterButton, type SFooterProps } from './SFooter';
1
+ export { SFooter, type SFooterBg, type SFooterButton, type SFooterProps, type SFooterSize, } from './SFooter';
@@ -19,6 +19,9 @@
19
19
  | `loadingSlot?` | `ReactNode` | — | 로딩 중 표시를 직접 지정한다 (기본: 스피너) |
20
20
  | `errorSlot?` | `ReactNode` | — | 실패·빈 상태 표시를 직접 지정한다 (기본: 빈 이미지 아이콘) |
21
21
  | `imgProps?` | `ImgHTMLAttributes<HTMLImageElement>` | — | 안쪽 `<img>` 로 넘길 네이티브 속성 (srcSet · sizes · crossOrigin · draggable 등) |
22
+ | `previewOnHover?` | `boolean` | `false` | 호버하면 큰 이미지를 포털로 띄운다. 썸네일이 작아 내용을 알아볼 수 없는 자리에 쓴다 |
23
+ | `previewSrc?` | `string` | — | 미리보기에 띄울 경로. 원본 URL 이 썸네일과 다를 때 준다 (기본: `src` 와 같은 경로) |
24
+ | `previewSize?` | `number` | `PREVIEW_MAX_SIZE` | 미리보기 상자의 한 변(px). 상자는 정사각형이고 이미지는 `contain` 으로 들어간다. **200px 이 상한이라 더 큰 값을 줘도 200 으로 잘린다** — 미리보기가 화면을 덮지 않게 하는 규격이다. |
22
25
 
23
26
  #### Events
24
27
 
@@ -27,15 +27,18 @@ export interface SImageProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoa
27
27
  errorSlot?: ReactNode;
28
28
  /** 안쪽 `<img>` 로 넘길 네이티브 속성 (srcSet · sizes · crossOrigin · draggable 등) */
29
29
  imgProps?: ImgHTMLAttributes<HTMLImageElement>;
30
+ /** 호버하면 큰 이미지를 포털로 띄운다. 썸네일이 작아 내용을 알아볼 수 없는 자리에 쓴다 */
31
+ previewOnHover?: boolean;
32
+ /** 미리보기에 띄울 경로. 원본 URL 이 썸네일과 다를 때 준다 (기본: `src` 와 같은 경로) */
33
+ previewSrc?: string;
34
+ /**
35
+ * 미리보기 상자의 한 변(px). 상자는 정사각형이고 이미지는 `contain` 으로 들어간다.
36
+ * **200px 이 상한이라 더 큰 값을 줘도 200 으로 잘린다** — 미리보기가 화면을 덮지 않게 하는 규격이다.
37
+ */
38
+ previewSize?: number;
30
39
  /** 로드 완료. 로드된 src 를 넘긴다 */
31
40
  onLoad?: (src: string) => void;
32
41
  /** 로드 실패 */
33
42
  onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
34
43
  }
35
- /**
36
- * SImage — 이미지 로딩·실패 상태를 스스로 표현하는 이미지 상자.
37
- *
38
- * 로딩 중에는 스피너, `src` 가 없거나 로드에 실패하면 빈 이미지 아이콘을 컨테이너 높이에
39
- * 비례한 크기로 가운데에 그린다.
40
- */
41
44
  export declare const SImage: import("react").ForwardRefExoticComponent<SImageProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -1,2 +1,2 @@
1
1
  export { SLoadingContainer, type SLoadingContainerProps } from './SLoadingContainer';
2
- export { loading, SLoadingViewport } from './loading';
2
+ export { loading, type SLoadingHandle } from './loading';
@@ -1,26 +1,45 @@
1
- /** 오버레이 뷰포트 — 스토어를 구독해 SLoadingContainer 를 렌더한다. 직접 배치도 가능. */
2
- export declare function SLoadingViewport(): import("react").JSX.Element;
1
+ /**
2
+ * `show()` 가 돌려주는 핸들 — **이 호출 하나**만 걷고 갱신한다.
3
+ * 부르면 걷고, `update()` 로 이 호출의 메시지만 바꾼다. 둘 다 id 기준이라 겹친 다른 작업의
4
+ * 항목은 건드리지 않는다. (Quasar `Loading.show()` 가 돌려주는 그룹 함수와 같은 자리다.)
5
+ */
6
+ export interface SLoadingHandle {
7
+ (): void;
8
+ update(options: {
9
+ message?: string;
10
+ }): void;
11
+ }
3
12
  export declare const loading: {
4
13
  /**
5
14
  * 로딩 오버레이 표시. 겹쳐 부르면 그만큼 쌓이고 **마지막 하나가 걷힐 때** 닫힌다.
6
- * 반환값은 이 호출 하나만 걷는 함수다(두 번 불러도 한 번만 걷힌다) — `hide()` 대신 이것을
7
- * 쓰면 짝이 어긋나지 않는다.
15
+ * 반환값은 이 호출 하나를 걷고 갱신하는 핸들이다(두 번 걷어도 한 번만 걷힌다).
16
+ *
17
+ * **걷는 것은 `finally` 에 둔다.** 빠뜨리면 그 즉시 화면이 갇히고, 정상 경로에서는 아무
18
+ * 표시가 없다.
8
19
  *
9
- * ⚠️ 메시지만 바꾸려면 `update()` 를 쓴다. `show()` 를 다시 부르면 하나 더 쌓여
10
- * `hide()` 한 번으로는 닫히지 않는다.
20
+ * ```ts
21
+ * const done = loading.show({ message: '1/2 ...' });
22
+ * try {
23
+ * done.update({ message: '2/2 ...' }); // 이 호출의 메시지만 바뀐다
24
+ * await api.submit(payload);
25
+ * } finally {
26
+ * done(); // 이 호출만 걷는다
27
+ * }
28
+ * ```
29
+ *
30
+ * ⚠️ **메시지를 바꾸려고 `show()` 를 다시 부르지 않는다.** 하나 더 쌓여 한 번 걷는 것으로는
31
+ * 닫히지 않는다. 핸들의 `update()` 를 쓴다. (참조 카운트 이전에는 `show()` 를 다시 부르는
32
+ * 것이 갱신이었다 — Quasar 도 같은 그룹에 다시 show 하면 갱신이다. 그 시절 관행대로 쓰인
33
+ * 코드는 지금 오버레이를 하나 남긴다.)
11
34
  */
12
35
  show(options?: {
13
36
  message?: string;
14
- }): () => void;
15
- /** 표시 중인 메시지 갱신 — 쌓인 수는 그대로다 */
16
- update(options: {
17
- message?: string;
18
- }): void;
19
- /** 가장 나중의 show 하나를 걷는다. 남은 것이 없을 때만 오버레이가 닫힌다 */
20
- hide(): void;
37
+ }): SLoadingHandle;
21
38
  /**
22
- * 쌓인 것을 모두 걷고 즉시 닫는다. 화면 전환·에러 복구처럼 진행 중이던 작업의 짝을 더는
23
- * 맞출 수 없을 때만 쓴다 — 평소에는 `hide()` 나 `show()` 가 준 함수로 하나씩 걷는다.
39
+ * 쌓인 것을 모두 걷고 즉시 닫는다. **원본(Quasar)의 인자 없는 `hide()` 가 이것이다.**
40
+ * 화면 전환·에러 복구처럼 진행 중이던 작업의 짝을 더는 맞출 수 없을 때만 쓴다 — 평소에는
41
+ * `show()` 가 준 핸들로 걷는다.
42
+ * 이걸로 걷힌 항목의 주인이 뒤늦게 자기 핸들을 불러도 아무 일도 일어나지 않는다.
24
43
  */
25
44
  hideAll(): void;
26
45
  };