sellmate-design-system-react 9.0.0-beta.79 → 9.0.0-beta.80

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.
@@ -11,10 +11,10 @@
11
11
  | `title` | `ReactNode` | — | |
12
12
  | `supportingText?` | `ReactNode` | — | 제목을 보조하는 텍스트 |
13
13
  | `supportingTextPosition?` | `SExpansionItemSupportingTextPosition` | `'right'` | 보조 텍스트 위치 |
14
- | `depth?` | `1 \| 2 \| 3` | `1` | 중첩 단계. Figma 기준 1~3단계 |
14
+ | `depth?` | `1 \| 2 \| 3` | `1` | 중첩 단계. Figma 기준 1~3단계. `SExpansionList` 안에서는 주지 않아도 중첩에서 정해진다 — 직접 주면 그 값이 우선한다. |
15
15
  | `leading?` | `ReactNode \| SExpansionItemRenderProp` | — | 타이틀 앞에 표시할 아이콘/콘텐츠 |
16
16
  | `trailing?` | `ReactNode` | — | 타이틀 뒤에 표시할 태그/콘텐츠 |
17
- | `bodyPadding?` | `boolean` | `true` | body 내부 패딩 사용 여부 |
17
+ | `bodyPadding?` | `boolean` | `true` | body 내부 패딩 사용 여부. `SExpansionList` 안에서 본문이 목록(`SListItem`·`SExpansionItem` 만, 또는 `SExpansionList`)이면 기본으로 꺼진다. |
18
18
  | `bordered?` | `boolean` | `false` | 외곽 테두리 사용 여부 |
19
19
  | `expanded?` | `boolean` | — | |
20
20
  | `defaultExpanded?` | `boolean` | `false` | |
@@ -14,13 +14,19 @@ export interface SExpansionItemProps extends Omit<HTMLAttributes<HTMLDivElement>
14
14
  supportingText?: ReactNode;
15
15
  /** 보조 텍스트 위치 */
16
16
  supportingTextPosition?: SExpansionItemSupportingTextPosition;
17
- /** 중첩 단계. Figma 기준 1~3단계 */
17
+ /**
18
+ * 중첩 단계. Figma 기준 1~3단계.
19
+ * `SExpansionList` 안에서는 주지 않아도 중첩에서 정해진다 — 직접 주면 그 값이 우선한다.
20
+ */
18
21
  depth?: 1 | 2 | 3;
19
22
  /** 타이틀 앞에 표시할 아이콘/콘텐츠 */
20
23
  leading?: ReactNode | SExpansionItemRenderProp;
21
24
  /** 타이틀 뒤에 표시할 태그/콘텐츠 */
22
25
  trailing?: ReactNode;
23
- /** body 내부 패딩 사용 여부 */
26
+ /**
27
+ * body 내부 패딩 사용 여부.
28
+ * `SExpansionList` 안에서 본문이 목록(`SListItem`·`SExpansionItem` 만, 또는 `SExpansionList`)이면 기본으로 꺼진다.
29
+ */
24
30
  bodyPadding?: boolean;
25
31
  /** 외곽 테두리 사용 여부 */
26
32
  bordered?: boolean;
@@ -11,12 +11,12 @@
11
11
  | `title` | `SListItemSlot` | — | 리스트 아이템 제목 |
12
12
  | `supportingText?` | `SListItemSlot` | — | 제목을 보조하는 텍스트 |
13
13
  | `supportingTextPosition?` | `SListItemSupportingTextPosition` | `'right'` | 보조 텍스트 위치 |
14
- | `depth?` | `1 \| 2 \| 3` | `1` | 중첩 단계. Figma 기준 1~3단계 |
14
+ | `depth?` | `1 \| 2 \| 3` | `1` | 중첩 단계. Figma 기준 1~3단계. `SExpansionList` 안에서는 주지 않아도 중첩에서 정해진다 — 직접 주면 그 값이 우선한다. |
15
15
  | `leading?` | `SListItemSlot` | — | 타이틀 앞에 표시할 아이콘/콘텐츠 |
16
16
  | `trailing?` | `SListItemSlot` | — | 타이틀 뒤에 표시할 태그/콘텐츠 |
17
17
  | `bordered?` | `boolean` | `false` | 외곽 테두리 사용 여부 |
18
18
  | `selected?` | `boolean` | `false` | 선택 상태 여부. `clickable` 이 false 면 무시된다 |
19
- | `clickable?` | `boolean` | `false` | 클릭 가능 상태 여부. hover·selected·chevron 등 인터랙션 표현이 여기에 딸린다 |
19
+ | `clickable?` | `boolean` | `false` | 클릭 가능 상태 여부. hover·selected·chevron 등 인터랙션 표현이 여기에 딸린다. `SExpansionList` 안에서는 기본으로 켜진다 — 표시 전용이면 `false` 를 준다. |
20
20
  | `interaction?` | `SListItemInteraction` | — | hover/selected 상태에서 적용할 인터랙션 preset |
21
21
  | `accentStripe?` | `boolean` | `false` | 아이템 왼쪽 accent stripe 표시 여부 |
22
22
  | `dense?` | `boolean` | `false` | 조밀한 높이 사용 여부 |
@@ -15,7 +15,10 @@ export interface SListItemProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ch
15
15
  supportingText?: SListItemSlot;
16
16
  /** 보조 텍스트 위치 */
17
17
  supportingTextPosition?: SListItemSupportingTextPosition;
18
- /** 중첩 단계. Figma 기준 1~3단계 */
18
+ /**
19
+ * 중첩 단계. Figma 기준 1~3단계.
20
+ * `SExpansionList` 안에서는 주지 않아도 중첩에서 정해진다 — 직접 주면 그 값이 우선한다.
21
+ */
19
22
  depth?: 1 | 2 | 3;
20
23
  /** 타이틀 앞에 표시할 아이콘/콘텐츠 */
21
24
  leading?: SListItemSlot;
@@ -25,7 +28,10 @@ export interface SListItemProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ch
25
28
  bordered?: boolean;
26
29
  /** 선택 상태 여부. `clickable` 이 false 면 무시된다 */
27
30
  selected?: boolean;
28
- /** 클릭 가능 상태 여부. hover·selected·chevron 등 인터랙션 표현이 여기에 딸린다 */
31
+ /**
32
+ * 클릭 가능 상태 여부. hover·selected·chevron 등 인터랙션 표현이 여기에 딸린다.
33
+ * `SExpansionList` 안에서는 기본으로 켜진다 — 표시 전용이면 `false` 를 준다.
34
+ */
29
35
  clickable?: boolean;
30
36
  /** hover/selected 상태에서 적용할 인터랙션 preset */
31
37
  interaction?: SListItemInteraction;
@@ -0,0 +1,55 @@
1
+ # SSkeleton
2
+
3
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
+
5
+ ### SSkeleton
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `type?` | `SSkeletonType` | `'rect'` | 자리표시 모양 - `rect`: 사각형. 폭은 부모를, 높이는 한 줄 높이를 따른다 — 이미지·카드 영역은 `height` 를 준다 - `circle`: 원형(아바타 등) - `text`: 텍스트 한 줄. 부모 타이포의 line-height 를 따르고 세로로 절반만 칠한다 - `button`·`input`·`tag`·`checkbox`·`radio`·`switch`: 해당 컴포넌트 기본 size 의 크기·모서리 |
12
+ | `animation?` | `SSkeletonAnimation` | `'wave'` | 로딩 애니메이션 - `wave`: 하이라이트 띠가 왼쪽에서 오른쪽으로 지나간다 - `pulse`: 크기가 숨쉬듯 줄었다 커진다. 블록 전체가 움직이므로 아바타처럼 작은 모양에만 쓴다 - `fade`: 투명도가 숨쉬듯 바뀐다 - `none`: 애니메이션 없음 OS 의 동작 줄이기 설정이 켜져 있으면 어떤 값이든 멈춘다. |
13
+ | `animationSpeed?` | `number` | `1500` | 애니메이션 한 주기 (ms) |
14
+ | `square?` | `boolean` | `false` | 모서리를 각지게 |
15
+ | `bordered?` | `boolean` | `false` | 테두리 |
16
+ | `size?` | `string \| number` | — | 가로·세로를 한 번에 지정. `width`·`height` 가 있으면 그쪽이 우선한다. 숫자는 px |
17
+ | `width?` | `string \| number` | — | 폭. 숫자는 px |
18
+ | `height?` | `string \| number` | — | 높이. 숫자는 px |
19
+
20
+ ## Types
21
+
22
+ ### SSkeletonType
23
+
24
+ ```ts
25
+ export type SSkeletonType = (typeof SKELETON_TYPES)[number];
26
+ ```
27
+
28
+ ### SSkeletonAnimation
29
+
30
+ ```ts
31
+ export type SSkeletonAnimation = (typeof SKELETON_ANIMATIONS)[number];
32
+ ```
33
+
34
+ ### SKELETON_TYPES
35
+
36
+ ```ts
37
+ export const SKELETON_TYPES = [
38
+ 'rect',
39
+ 'circle',
40
+ 'text',
41
+ 'button',
42
+ 'input',
43
+ 'tag',
44
+ 'checkbox',
45
+ 'radio',
46
+ 'switch',
47
+ ] as const;
48
+ ```
49
+
50
+ ### SKELETON_ANIMATIONS
51
+
52
+ ```ts
53
+ export const SKELETON_ANIMATIONS = ['wave', 'pulse', 'fade', 'none'] as const;
54
+ ```
55
+
@@ -0,0 +1,39 @@
1
+ import { type HTMLAttributes } from 'react';
2
+ import { type SSkeletonAnimation, type SSkeletonType } from './skeleton.config';
3
+ export interface SSkeletonProps extends HTMLAttributes<HTMLDivElement> {
4
+ /**
5
+ * 자리표시 모양
6
+ * - `rect`: 사각형. 폭은 부모를, 높이는 한 줄 높이를 따른다 — 이미지·카드 영역은 `height` 를 준다
7
+ * - `circle`: 원형(아바타 등)
8
+ * - `text`: 텍스트 한 줄. 부모 타이포의 line-height 를 따르고 세로로 절반만 칠한다
9
+ * - `button`·`input`·`tag`·`checkbox`·`radio`·`switch`: 해당 컴포넌트 기본 size 의 크기·모서리
10
+ */
11
+ type?: SSkeletonType;
12
+ /**
13
+ * 로딩 애니메이션
14
+ * - `wave`: 하이라이트 띠가 왼쪽에서 오른쪽으로 지나간다
15
+ * - `pulse`: 크기가 숨쉬듯 줄었다 커진다. 블록 전체가 움직이므로 아바타처럼 작은 모양에만 쓴다
16
+ * - `fade`: 투명도가 숨쉬듯 바뀐다
17
+ * - `none`: 애니메이션 없음
18
+ *
19
+ * OS 의 동작 줄이기 설정이 켜져 있으면 어떤 값이든 멈춘다.
20
+ */
21
+ animation?: SSkeletonAnimation;
22
+ /** 애니메이션 한 주기 (ms) */
23
+ animationSpeed?: number;
24
+ /** 모서리를 각지게 */
25
+ square?: boolean;
26
+ /** 테두리 */
27
+ bordered?: boolean;
28
+ /** 가로·세로를 한 번에 지정. `width`·`height` 가 있으면 그쪽이 우선한다. 숫자는 px */
29
+ size?: string | number;
30
+ /** 폭. 숫자는 px */
31
+ width?: string | number;
32
+ /** 높이. 숫자는 px */
33
+ height?: string | number;
34
+ }
35
+ /**
36
+ * SSkeleton — 데이터가 오기 전 콘텐츠 자리를 같은 모양으로 채워 두는 자리표시. Quasar QSkeleton 모티브.
37
+ * 장식이라 보조기기에는 숨긴다(`aria-hidden`). 로딩 중이라는 사실은 감싸는 영역에 `aria-busy` 로 알린다.
38
+ */
39
+ export declare const SSkeleton: import("react").ForwardRefExoticComponent<SSkeletonProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,2 @@
1
+ export { SSkeleton, type SSkeletonProps } from './SSkeleton';
2
+ export { SKELETON_TYPES, SKELETON_ANIMATIONS, SKELETON_TYPE_CONFIG, type SSkeletonType, type SSkeletonAnimation, } from './skeleton.config';
@@ -0,0 +1,22 @@
1
+ export declare const SKELETON_TYPES: readonly ["rect", "circle", "text", "button", "input", "tag", "checkbox", "radio", "switch"];
2
+ export type SSkeletonType = (typeof SKELETON_TYPES)[number];
3
+ export declare const SKELETON_ANIMATIONS: readonly ["wave", "pulse", "fade", "none"];
4
+ export type SSkeletonAnimation = (typeof SKELETON_ANIMATIONS)[number];
5
+ /**
6
+ * type 별 기본 크기·모서리. 값이 없는 축은 내용(공백 한 글자)과 부모 폭을 따른다.
7
+ * DS 모양(button·input…)은 해당 컴포넌트의 **기본 size** 토큰을 그대로 가리켜
8
+ * 토큰이 바뀌어도 실제 컴포넌트와 어긋나지 않는다. 폭은 토큰이 없어 Quasar 기본값을 따른다.
9
+ */
10
+ export declare const SKELETON_TYPE_CONFIG: Record<SSkeletonType, {
11
+ width?: string;
12
+ height?: string;
13
+ radius: string;
14
+ }>;
15
+ export declare const SKELETON_COLORS: {
16
+ /** Quasar rgba(0,0,0,.12) ≈ 흰 바탕 위 #E1E1E1 */
17
+ bg: string;
18
+ /** Quasar rgba(0,0,0,.05) 테두리 — 배경보다 한 단계 짙게 */
19
+ border: string;
20
+ /** Quasar rgba(255,255,255,.5) 하이라이트 띠 */
21
+ wave: string;
22
+ };
package/dist/index.cjs CHANGED
@@ -5689,6 +5689,8 @@ var SChatSystemMessageImpl = /* @__PURE__ */ react.forwardRef(
5689
5689
  const notices = message ?? [];
5690
5690
  return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: cn("flex flex-col items-center gap-sd-8", className), ...rest, children: notices.map((notice, i) => (
5691
5691
  // 알약은 문구 길이만큼만 차지하고, 길면 줄바꿈해 여러 줄로 선다.
5692
+ // inline-flex 로 두면 안의 글자와 <b> 같은 인라인 태그가 각각 flex 아이템이 되어 그 사이 띄어쓰기가
5693
+ // 사라진다(`상담사 <b>김셀메</b>가` → 상담사김셀메가). 글 흐름 그대로 두려고 inline-block 이다.
5692
5694
  // 안내는 HTML 문자열을 허용하되 sanitizeInlineHtml 로 살균 후 innerHTML 렌더 (XSS 방어)
5693
5695
  /* @__PURE__ */ jsxRuntime.jsx(
5694
5696
  "span",
@@ -5697,7 +5699,7 @@ var SChatSystemMessageImpl = /* @__PURE__ */ react.forwardRef(
5697
5699
  // 라운드는 말풍선과 같은 토큰을 쓴다 — 여러 줄이 돼도 모서리가 상자 높이를 따라
5698
5700
  // 부풀지 않는다. 제 몫인 chatSystemMessage.radius 는 pill(9999px)이라 쓰지 않는다.
5699
5701
  // 두 줄부터 좌우가 커다란 반원이 되어 첫 글자가 안으로 밀리기 때문이다.
5700
- "inline-flex max-w-full rounded-(--cmp-chatBubble-radius) border",
5702
+ "inline-block max-w-full rounded-(--cmp-chatBubble-radius) border",
5701
5703
  "border-(--cmp-chatSystemMessage-border-color) bg-(--cmp-chatSystemMessage-bg)",
5702
5704
  "typo-body-sm-medium text-(--cmp-chatSystemMessage-color)",
5703
5705
  "px-(--cmp-chatSystemMessage-paddingX) py-(--cmp-chatSystemMessage-paddingY) text-center",
@@ -6851,17 +6853,26 @@ var SListItemImpl = /* @__PURE__ */ react.forwardRef(function SListItem({
6851
6853
  );
6852
6854
  });
6853
6855
  var SListItem2 = /* @__PURE__ */ withDisplayName(SListItemImpl, "SListItem");
6856
+ var SExpansionListDepthContext = /* @__PURE__ */ react.createContext(null);
6857
+ var MAX_DEPTH = 3;
6854
6858
  function isExpansionItemElement(node) {
6855
6859
  return react.isValidElement(node) && node.type === SExpansionItem;
6856
6860
  }
6857
6861
  function isListItemElement(node) {
6858
6862
  return react.isValidElement(node) && node.type === SListItem2;
6859
6863
  }
6860
- function getInitialOpenKeys(children) {
6864
+ function isExpansionListElement(node) {
6865
+ return react.isValidElement(node) && node.type === SExpansionList2;
6866
+ }
6867
+ function isLevelBody(children) {
6868
+ const nodes = react.Children.toArray(children);
6869
+ return nodes.length > 0 && nodes.every((node) => isListItemElement(node) || isExpansionItemElement(node));
6870
+ }
6871
+ function getInitialOpenKeys(children, levelDepth) {
6861
6872
  const openKeys = {};
6862
6873
  react.Children.forEach(children, (child, index) => {
6863
6874
  if (!isExpansionItemElement(child)) return;
6864
- const depth = child.props.depth ?? 1;
6875
+ const depth = child.props.depth ?? levelDepth;
6865
6876
  if (openKeys[depth] != null) return;
6866
6877
  if (child.props.expanded === true || child.props.defaultExpanded === true) {
6867
6878
  openKeys[depth] = String(child.key ?? index);
@@ -6870,15 +6881,28 @@ function getInitialOpenKeys(children) {
6870
6881
  return openKeys;
6871
6882
  }
6872
6883
  var SExpansionListImpl = /* @__PURE__ */ react.forwardRef(function SExpansionList({ children, ...rest }, ref) {
6873
- const [openKeys, setOpenKeys] = react.useState(() => getInitialOpenKeys(children));
6884
+ const parentDepth = react.useContext(SExpansionListDepthContext);
6885
+ const levelDepth = parentDepth == null ? 1 : Math.min(parentDepth + 1, MAX_DEPTH);
6886
+ const [openKeys, setOpenKeys] = react.useState(
6887
+ () => getInitialOpenKeys(children, levelDepth)
6888
+ );
6874
6889
  const listChildren = react.Children.map(children, (child, index) => {
6875
6890
  if (isListItemElement(child)) {
6876
- return react.cloneElement(child, { clickable: child.props.clickable ?? true });
6891
+ return react.cloneElement(child, {
6892
+ clickable: child.props.clickable ?? true,
6893
+ depth: child.props.depth ?? levelDepth
6894
+ });
6877
6895
  }
6878
6896
  if (!isExpansionItemElement(child)) return child;
6879
- const depth = child.props.depth ?? 1;
6897
+ const depth = child.props.depth ?? levelDepth;
6880
6898
  const itemKey = String(child.key ?? index);
6899
+ const body = child.props.children;
6900
+ const bodyIsList = isExpansionListElement(body);
6901
+ const wrapBody = !bodyIsList && isLevelBody(body);
6881
6902
  return react.cloneElement(child, {
6903
+ depth,
6904
+ bodyPadding: child.props.bodyPadding ?? (bodyIsList || wrapBody ? false : void 0),
6905
+ ...wrapBody && { children: /* @__PURE__ */ jsxRuntime.jsx(SExpansionListImpl, { children: body }) },
6882
6906
  expanded: openKeys[depth] === itemKey,
6883
6907
  onToggle: (next, event) => {
6884
6908
  setOpenKeys((current) => ({
@@ -6889,7 +6913,7 @@ var SExpansionListImpl = /* @__PURE__ */ react.forwardRef(function SExpansionLis
6889
6913
  }
6890
6914
  });
6891
6915
  });
6892
- return /* @__PURE__ */ jsxRuntime.jsx(SList2, { ref, ...rest, children: listChildren });
6916
+ return /* @__PURE__ */ jsxRuntime.jsx(SExpansionListDepthContext.Provider, { value: levelDepth, children: /* @__PURE__ */ jsxRuntime.jsx(SList2, { ref, ...rest, children: listChildren }) });
6893
6917
  });
6894
6918
  var SExpansionList2 = /* @__PURE__ */ withDisplayName(SExpansionListImpl, "SExpansionList");
6895
6919
 
@@ -14094,6 +14118,22 @@ function groupAdjacentItems(items) {
14094
14118
  []
14095
14119
  );
14096
14120
  }
14121
+ function scrollItemIntoContainer(item4) {
14122
+ const view = item4.ownerDocument.defaultView;
14123
+ if (!view) return;
14124
+ let container = item4.parentElement;
14125
+ while (container) {
14126
+ const { overflowY } = view.getComputedStyle(container);
14127
+ if ((overflowY === "auto" || overflowY === "scroll") && container.scrollHeight > container.clientHeight)
14128
+ break;
14129
+ container = container.parentElement;
14130
+ }
14131
+ if (!container) return;
14132
+ const box3 = container.getBoundingClientRect();
14133
+ const rect = item4.getBoundingClientRect();
14134
+ if (rect.top < box3.top) container.scrollTop -= box3.top - rect.top;
14135
+ else if (rect.bottom > box3.bottom) container.scrollTop += rect.bottom - box3.bottom;
14136
+ }
14097
14137
  var SStepper = /* @__PURE__ */ react.forwardRef(function SStepper2({
14098
14138
  items,
14099
14139
  value,
@@ -14127,10 +14167,16 @@ var SStepper = /* @__PURE__ */ react.forwardRef(function SStepper2({
14127
14167
  const resolvedClickableIndex = lastCompletedIndex + 1;
14128
14168
  const [visitedMaxIndex, setVisitedMaxIndex] = react.useState(activeIndex);
14129
14169
  const firstErrorIndex = items.findIndex((item4) => item4.error === true);
14170
+ const firstErrorIndexRef = react.useRef(firstErrorIndex);
14171
+ firstErrorIndexRef.current = firstErrorIndex;
14130
14172
  react.useImperativeHandle(
14131
14173
  ref,
14132
14174
  () => ({
14133
- showItemTooltip: () => setCustomTooltipOpen(true),
14175
+ showItemTooltip: () => {
14176
+ const errorItem = verticalItemRefs.current[firstErrorIndexRef.current];
14177
+ if (errorItem) scrollItemIntoContainer(errorItem);
14178
+ setCustomTooltipOpen(true);
14179
+ },
14134
14180
  hideItemTooltip: () => setCustomTooltipOpen(false)
14135
14181
  }),
14136
14182
  []
@@ -14154,6 +14200,11 @@ var SStepper = /* @__PURE__ */ react.forwardRef(function SStepper2({
14154
14200
  transform: `translateY(${activeItem.offsetTop}px)`
14155
14201
  });
14156
14202
  }, [activeIndex, vertical, items]);
14203
+ react.useLayoutEffect(() => {
14204
+ if (!vertical) return;
14205
+ const activeItem = verticalItemRefs.current[activeIndex];
14206
+ if (activeItem) scrollItemIntoContainer(activeItem);
14207
+ }, [activeIndex, vertical]);
14157
14208
  const getState = (item4, index) => {
14158
14209
  if (index === activeIndex) return "active";
14159
14210
  if (item4.completed != null) return item4.completed ? "completed" : "default";
@@ -14274,6 +14325,9 @@ var SStepper = /* @__PURE__ */ react.forwardRef(function SStepper2({
14274
14325
  className: cn("relative inline-flex flex-col items-start", className),
14275
14326
  style: {
14276
14327
  width: STEPPER_VERTICAL_LAYOUT.width,
14328
+ // 폭은 토큰이 정하되 부모보다 넓어지지 않는다 — 부모가 세로 스크롤바만큼 좁아지면 따라 줄어
14329
+ // 가로 넘침(가로 스크롤바)이 생기지 않는다
14330
+ maxWidth: "100%",
14277
14331
  fontSize: config.fontSize,
14278
14332
  lineHeight: config.lineHeight,
14279
14333
  ...style
@@ -22581,6 +22635,113 @@ var SToastContainer = /* @__PURE__ */ react.forwardRef(
22581
22635
  );
22582
22636
  }
22583
22637
  );
22638
+
22639
+ // src/components/SSkeleton/skeleton.config.ts
22640
+ var SKELETON_TYPES = [
22641
+ "rect",
22642
+ "circle",
22643
+ "text",
22644
+ "button",
22645
+ "input",
22646
+ "tag",
22647
+ "checkbox",
22648
+ "radio",
22649
+ "switch"
22650
+ ];
22651
+ var SKELETON_ANIMATIONS = ["wave", "pulse", "fade", "none"];
22652
+ var SKELETON_TYPE_CONFIG = {
22653
+ rect: { radius: "var(--sys-radius-control-sm)" },
22654
+ circle: { width: "48px", height: "48px", radius: "50%" },
22655
+ text: { radius: "var(--sys-radius-control-sm)" },
22656
+ button: {
22657
+ width: "90px",
22658
+ height: "var(--cmp-button-sm-height)",
22659
+ radius: "var(--cmp-button-radius-sm)"
22660
+ },
22661
+ input: { height: "var(--cmp-textinput-sm-height)", radius: "var(--cmp-textinput-sm-radius)" },
22662
+ tag: { width: "70px", height: "var(--cmp-tag-sm-height)", radius: "var(--cmp-tag-sm-radius)" },
22663
+ checkbox: {
22664
+ width: "var(--cmp-checkbox-size)",
22665
+ height: "var(--cmp-checkbox-size)",
22666
+ radius: "var(--cmp-checkbox-radius)"
22667
+ },
22668
+ radio: {
22669
+ width: "var(--cmp-radio-size)",
22670
+ height: "var(--cmp-radio-size)",
22671
+ radius: "var(--cmp-radio-radius)"
22672
+ },
22673
+ switch: {
22674
+ width: "var(--cmp-switch-width)",
22675
+ height: "var(--cmp-switch-height)",
22676
+ radius: "var(--cmp-switch-radius)"
22677
+ }
22678
+ };
22679
+ var SKELETON_COLORS = {
22680
+ /** Quasar rgba(0,0,0,.12) ≈ 흰 바탕 위 #E1E1E1 */
22681
+ bg: "var(--sys-color-disabled-bg)",
22682
+ /** Quasar rgba(0,0,0,.05) 테두리 — 배경보다 한 단계 짙게 */
22683
+ border: "color-mix(in srgb, var(--sys-color-border-strong) 25%, transparent)",
22684
+ /** Quasar rgba(255,255,255,.5) 하이라이트 띠 */
22685
+ wave: "linear-gradient(90deg, transparent, color-mix(in srgb, var(--sys-color-bg-frame) 50%, transparent), transparent)"
22686
+ };
22687
+ var ANIMATION_CLASS = {
22688
+ pulse: "animate-skeleton-pulse",
22689
+ fade: "animate-skeleton-fade"
22690
+ };
22691
+ var toCss = (v) => typeof v === "number" ? `${v}px` : v;
22692
+ var SSkeleton = /* @__PURE__ */ react.forwardRef(function SSkeleton2({
22693
+ type = "rect",
22694
+ animation = "wave",
22695
+ animationSpeed = 1500,
22696
+ square = false,
22697
+ bordered = false,
22698
+ size,
22699
+ width,
22700
+ height,
22701
+ className,
22702
+ style,
22703
+ ...props
22704
+ }, ref) {
22705
+ const t = SKELETON_TYPE_CONFIG[type];
22706
+ const duration = `${animationSpeed}ms`;
22707
+ return /* @__PURE__ */ jsxRuntime.jsxs(
22708
+ "div",
22709
+ {
22710
+ ref,
22711
+ "aria-hidden": "true",
22712
+ ...props,
22713
+ className: cn(
22714
+ "box-border cursor-wait select-none",
22715
+ // text 는 transform 이 아니라 scale 속성으로 줄여서 pulse 의 transform 애니메이션과 겹치지 않는다
22716
+ type === "text" && "scale-y-50",
22717
+ bordered && "border border-solid",
22718
+ animation === "wave" && "relative overflow-hidden",
22719
+ animation !== "wave" && animation !== "none" && ANIMATION_CLASS[animation],
22720
+ "motion-reduce:animate-none",
22721
+ className
22722
+ ),
22723
+ style: {
22724
+ width: toCss(width ?? size) ?? t.width,
22725
+ height: toCss(height ?? size) ?? t.height,
22726
+ borderRadius: square ? 0 : t.radius,
22727
+ background: SKELETON_COLORS.bg,
22728
+ borderColor: bordered ? SKELETON_COLORS.border : void 0,
22729
+ animationDuration: animation === "pulse" || animation === "fade" ? duration : void 0,
22730
+ ...style
22731
+ },
22732
+ children: [
22733
+ "\xA0",
22734
+ animation === "wave" && /* @__PURE__ */ jsxRuntime.jsx(
22735
+ "span",
22736
+ {
22737
+ className: "absolute inset-0 animate-skeleton-wave motion-reduce:hidden",
22738
+ style: { background: SKELETON_COLORS.wave, animationDuration: duration }
22739
+ }
22740
+ )
22741
+ ]
22742
+ }
22743
+ );
22744
+ });
22584
22745
  var SForm = /* @__PURE__ */ react.forwardRef(function SForm2({ formClassName, onSubmit, onValidationError, children, className, style }, ref) {
22585
22746
  const formRef = react.useRef(null);
22586
22747
  const fieldsRef = react.useRef(/* @__PURE__ */ new Set());
@@ -28436,6 +28597,9 @@ exports.SGuide = SGuide;
28436
28597
  exports.SIcon = SIcon;
28437
28598
  exports.SImage = SImage;
28438
28599
  exports.SInput = SInput;
28600
+ exports.SKELETON_ANIMATIONS = SKELETON_ANIMATIONS;
28601
+ exports.SKELETON_TYPES = SKELETON_TYPES;
28602
+ exports.SKELETON_TYPE_CONFIG = SKELETON_TYPE_CONFIG;
28439
28603
  exports.SKeyValueTable = SKeyValueTable;
28440
28604
  exports.SLAYOUT_MIN_WIDTH = SLAYOUT_MIN_WIDTH;
28441
28605
  exports.SLauncherListBox = SLauncherListBox;
@@ -28470,6 +28634,7 @@ exports.SScrollArea = SScrollArea;
28470
28634
  exports.SSearchInput = SSearchInput;
28471
28635
  exports.SSectionHeaderCard = SSectionHeaderCard2;
28472
28636
  exports.SSelect = SSelect2;
28637
+ exports.SSkeleton = SSkeleton;
28473
28638
  exports.SSplitter = SSplitter2;
28474
28639
  exports.SStepper = SStepper;
28475
28640
  exports.SSwitch = SSwitch;