sellmate-design-system-react 3.4.0 → 3.6.0

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 (60) hide show
  1. package/dist/components/SConfirmModal/README.md +1 -0
  2. package/dist/components/SConfirmModal/SConfirmModal.cjs +3 -1
  3. package/dist/components/SConfirmModal/SConfirmModal.cjs.map +1 -1
  4. package/dist/components/SConfirmModal/SConfirmModal.d.ts +3 -1
  5. package/dist/components/SConfirmModal/SConfirmModal.js +3 -1
  6. package/dist/components/SConfirmModal/SConfirmModal.js.map +1 -1
  7. package/dist/components/SDraggableItem/README.md +1 -0
  8. package/dist/components/SDraggableItem/SDraggableItem.cjs +2 -1
  9. package/dist/components/SDraggableItem/SDraggableItem.cjs.map +1 -1
  10. package/dist/components/SDraggableItem/SDraggableItem.d.ts +2 -0
  11. package/dist/components/SDraggableItem/SDraggableItem.js +2 -1
  12. package/dist/components/SDraggableItem/SDraggableItem.js.map +1 -1
  13. package/dist/components/SDraggableList/README.md +37 -0
  14. package/dist/components/SDraggableList/SDraggableList.cjs +143 -0
  15. package/dist/components/SDraggableList/SDraggableList.cjs.map +1 -0
  16. package/dist/components/SDraggableList/SDraggableList.d.ts +27 -0
  17. package/dist/components/SDraggableList/SDraggableList.js +141 -0
  18. package/dist/components/SDraggableList/SDraggableList.js.map +1 -0
  19. package/dist/components/SDraggableList/index.d.ts +1 -0
  20. package/dist/components/SExpansionItem/README.md +8 -0
  21. package/dist/components/SExpansionItem/SExpansionItem.cjs +52 -9
  22. package/dist/components/SExpansionItem/SExpansionItem.cjs.map +1 -1
  23. package/dist/components/SExpansionItem/SExpansionItem.d.ts +7 -0
  24. package/dist/components/SExpansionItem/SExpansionItem.js +53 -10
  25. package/dist/components/SExpansionItem/SExpansionItem.js.map +1 -1
  26. package/dist/components/SExpansionList/README.md +9 -0
  27. package/dist/components/SExpansionList/SExpansionList.cjs +3150 -0
  28. package/dist/components/SExpansionList/SExpansionList.cjs.map +1 -0
  29. package/dist/components/SExpansionList/SExpansionList.d.ts +3 -0
  30. package/dist/components/SExpansionList/SExpansionList.js +3148 -0
  31. package/dist/components/SExpansionList/SExpansionList.js.map +1 -0
  32. package/dist/components/SExpansionList/index.d.ts +1 -0
  33. package/dist/components/SList/README.md +15 -0
  34. package/dist/components/SListItem/README.md +21 -0
  35. package/dist/components/SListItem/SListItem.cjs +2858 -5
  36. package/dist/components/SListItem/SListItem.cjs.map +1 -1
  37. package/dist/components/SListItem/SListItem.d.ts +7 -0
  38. package/dist/components/SListItem/SListItem.js +2859 -6
  39. package/dist/components/SListItem/SListItem.js.map +1 -1
  40. package/dist/components/SModal/README.md +1 -1
  41. package/dist/components/SModal/SModal.cjs +3 -1
  42. package/dist/components/SModal/SModal.cjs.map +1 -1
  43. package/dist/components/SModal/SModal.js +3 -1
  44. package/dist/components/SModal/SModal.js.map +1 -1
  45. package/dist/components/SSectionHeaderCard/README.md +1 -0
  46. package/dist/components/SSectionHeaderCard/SSectionHeaderCard.cjs +15 -1
  47. package/dist/components/SSectionHeaderCard/SSectionHeaderCard.cjs.map +1 -1
  48. package/dist/components/SSectionHeaderCard/SSectionHeaderCard.d.ts +3 -0
  49. package/dist/components/SSectionHeaderCard/SSectionHeaderCard.js +15 -1
  50. package/dist/components/SSectionHeaderCard/SSectionHeaderCard.js.map +1 -1
  51. package/dist/components/SSectionHeaderCard/index.d.ts +1 -1
  52. package/dist/index.cjs +282 -33
  53. package/dist/index.cjs.map +1 -1
  54. package/dist/index.d.ts +2 -0
  55. package/dist/index.js +282 -35
  56. package/dist/index.js.map +1 -1
  57. package/dist/llms-full.txt +81 -1
  58. package/dist/llms.txt +2 -2
  59. package/dist/styles.css +59 -63
  60. package/package.json +1 -1
@@ -0,0 +1,37 @@
1
+ # SDraggableList
2
+
3
+ > 자동 생성 문서 — `npm run docs:gen`. 소스: 각 컴포넌트의 Props/Handle 인터페이스 + import 의존성.
4
+
5
+ ### SDraggableList
6
+
7
+ #### Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `items` | `T[]` | — | 현재 순서대로 렌더링할 아이템 목록 |
12
+ | `getKey` | `(item: T, index: number) => string` | — | 아이템을 식별할 안정적인 key |
13
+ | `renderItem` | `(item: T, index: number, state: SDraggableListRenderState) => ReactNode` | — | 아이템 렌더링 함수 |
14
+ | `selectedKey?` | `string` | — | 외부에서 제어하는 selected 아이템 key |
15
+ | `defaultSelectedKey?` | `string` | — | 초기 selected 아이템 key |
16
+ | `getDisabled?` | `(item: T, index: number) => boolean` | — | disabled 아이템은 선택 및 드래그에서 제외 |
17
+
18
+ #### Events
19
+
20
+ | Event | Type | Description |
21
+ |-------|------|-------------|
22
+ | `onChange` | `(items: T[]) => void` | 드래그가 끝난 뒤 변경된 순서 |
23
+ | `onSelectedKeyChange` | `(key: string) => void` | selected 아이템이 변경될 때 호출 |
24
+
25
+ ## Dependencies
26
+
27
+ ### Depends on
28
+
29
+ - [SList](../SList)
30
+
31
+ ### Graph
32
+
33
+ ```mermaid
34
+ graph TD;
35
+ SDraggableList --> SList
36
+ style SDraggableList fill:#f9f,stroke:#333,stroke-width:4px
37
+ ```
@@ -0,0 +1,143 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var clsx = require('clsx');
6
+ var tailwindMerge = require('tailwind-merge');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+
9
+ var twMerge = tailwindMerge.extendTailwindMerge({
10
+ extend: {
11
+ classGroups: {
12
+ "font-size": [{ text: ["10", "11", "12", "13", "14", "16", "18", "20", "24", "28", "32"] }]
13
+ }
14
+ }
15
+ });
16
+ function cn(...inputs) {
17
+ return twMerge(clsx.clsx(inputs));
18
+ }
19
+ var SList = react.forwardRef(function SList2({ className, children, useGap = false, usePadding = false, ...rest }, ref) {
20
+ return /* @__PURE__ */ jsxRuntime.jsx(
21
+ "div",
22
+ {
23
+ ref,
24
+ className: cn(
25
+ useGap && "flex flex-col gap-(--cmp-list-group-gap)",
26
+ usePadding && "p-(--cmp-list-group-padding)",
27
+ className
28
+ ),
29
+ ...rest,
30
+ children
31
+ }
32
+ );
33
+ });
34
+ SList.displayName = "SList";
35
+ function reorder(items, fromIndex, toIndex) {
36
+ if (fromIndex === toIndex) return items;
37
+ const nextItems = [...items];
38
+ const [movedItem] = nextItems.splice(fromIndex, 1);
39
+ nextItems.splice(toIndex, 0, movedItem);
40
+ return nextItems;
41
+ }
42
+ function SDraggableListInner({
43
+ items,
44
+ getKey,
45
+ renderItem,
46
+ onChange,
47
+ selectedKey,
48
+ defaultSelectedKey,
49
+ onSelectedKeyChange,
50
+ getDisabled,
51
+ ...rest
52
+ }, ref) {
53
+ const [draggingIndex, setDraggingIndex] = react.useState(null);
54
+ const [dropIndex, setDropIndex] = react.useState(null);
55
+ const [internalSelectedKey, setInternalSelectedKey] = react.useState(
56
+ defaultSelectedKey
57
+ );
58
+ const listRef = react.useRef(null);
59
+ const fromIndexRef = react.useRef(null);
60
+ const currentSelectedKey = selectedKey ?? internalSelectedKey;
61
+ const startDrag = react.useCallback((event, index) => {
62
+ event.preventDefault();
63
+ fromIndexRef.current = index;
64
+ setDraggingIndex(index);
65
+ setDropIndex(index);
66
+ }, []);
67
+ react.useEffect(() => {
68
+ if (draggingIndex == null) return void 0;
69
+ const handleMouseMove = (event) => {
70
+ const itemElements = listRef.current?.querySelectorAll(
71
+ "[data-draggable-list-item]"
72
+ );
73
+ if (!itemElements) return;
74
+ let nextDropIndex = items.length;
75
+ itemElements.forEach((itemElement, index) => {
76
+ const rect = itemElement.getBoundingClientRect();
77
+ if (event.clientY < rect.top + rect.height / 2 && nextDropIndex === items.length) {
78
+ nextDropIndex = index;
79
+ }
80
+ });
81
+ setDropIndex(nextDropIndex);
82
+ };
83
+ const handleMouseUp = () => {
84
+ const fromIndex = fromIndexRef.current;
85
+ const nextDropIndex = dropIndex;
86
+ if (fromIndex != null && nextDropIndex != null) {
87
+ const toIndex = nextDropIndex > fromIndex ? nextDropIndex - 1 : nextDropIndex;
88
+ if (fromIndex !== toIndex) onChange(reorder(items, fromIndex, toIndex));
89
+ }
90
+ fromIndexRef.current = null;
91
+ setDraggingIndex(null);
92
+ setDropIndex(null);
93
+ };
94
+ window.addEventListener("mousemove", handleMouseMove);
95
+ window.addEventListener("mouseup", handleMouseUp);
96
+ return () => {
97
+ window.removeEventListener("mousemove", handleMouseMove);
98
+ window.removeEventListener("mouseup", handleMouseUp);
99
+ };
100
+ }, [draggingIndex, dropIndex, items, onChange]);
101
+ return /* @__PURE__ */ jsxRuntime.jsx(
102
+ SList,
103
+ {
104
+ ref: (node) => {
105
+ listRef.current = node;
106
+ if (typeof ref === "function") ref(node);
107
+ else if (ref) ref.current = node;
108
+ },
109
+ ...rest,
110
+ children: items.map((item, index) => {
111
+ const itemKey = getKey(item, index);
112
+ const disabled = getDisabled?.(item, index) ?? false;
113
+ const showBefore = draggingIndex != null && dropIndex === index;
114
+ const showAfter = draggingIndex != null && dropIndex === items.length && index === items.length - 1;
115
+ return /* @__PURE__ */ jsxRuntime.jsx(
116
+ "div",
117
+ {
118
+ "data-draggable-list-item": true,
119
+ "aria-disabled": disabled || void 0,
120
+ onClick: () => {
121
+ if (disabled) return;
122
+ setInternalSelectedKey(itemKey);
123
+ onSelectedKeyChange?.(itemKey);
124
+ },
125
+ className: `relative ${showBefore ? "before:absolute before:left-0 before:right-0 before:top-0 before:z-10 before:h-[2px] before:bg-(--sys-color-border-accent) before:content-['']" : ""} ${showAfter ? "after:absolute after:bottom-0 after:left-0 after:right-0 after:z-10 after:h-[2px] after:bg-(--sys-color-border-accent) after:content-['']" : ""}`,
126
+ children: renderItem(item, index, {
127
+ onDragHandleMouseDown: (event) => {
128
+ if (!disabled) startDrag(event, index);
129
+ },
130
+ selected: currentSelectedKey === itemKey
131
+ })
132
+ },
133
+ itemKey
134
+ );
135
+ })
136
+ }
137
+ );
138
+ }
139
+ var SDraggableList = react.forwardRef(SDraggableListInner);
140
+
141
+ exports.SDraggableList = SDraggableList;
142
+ //# sourceMappingURL=SDraggableList.cjs.map
143
+ //# sourceMappingURL=SDraggableList.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SList/SList.tsx","../../../src/components/SDraggableList/SDraggableList.tsx"],"names":["extendTailwindMerge","clsx","forwardRef","SList","jsx","useState","useRef","useCallback","useEffect"],"mappings":"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","file":"SDraggableList.cjs","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\n/**\n * theme.css 의 --text-* 는 기본 Tailwind 폰트사이즈 스케일(xs/sm/base...)이 아닌\n * 숫자 스케일(10~32)이라, 확장 없이는 tailwind-merge 가 이를 텍스트 컬러 그룹으로\n * 오인해 text-12 같은 클래스를 text-(--color-var) 뒤에서 충돌 제거해버린다.\n */\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [{ text: ['10', '11', '12', '13', '14', '16', '18', '20', '24', '28', '32'] }],\n },\n },\n});\n\n/**\n * className 병합 유틸.\n * clsx로 조건부 클래스를 합치고, tailwind-merge로 Tailwind 클래스 충돌을 해소한다.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport { forwardRef, type HTMLAttributes, type ReactNode } from 'react';\nimport { cn } from '../../lib/cn';\n\nexport interface SListProps extends HTMLAttributes<HTMLDivElement> {\n /** 리스트 컨테이너 내부에 렌더링할 내용 */\n children?: ReactNode;\n /** 리스트 아이템 사이 gap 토큰 적용 여부 */\n useGap?: boolean;\n /** 리스트 컨테이너 padding 토큰 적용 여부 */\n usePadding?: boolean;\n}\n\nexport const SList = forwardRef<HTMLDivElement, SListProps>(function SList(\n { className, children, useGap = false, usePadding = false, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cn(\n useGap && 'flex flex-col gap-(--cmp-list-group-gap)',\n usePadding && 'p-(--cmp-list-group-padding)',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n});\n\nSList.displayName = 'SList';\n","'use client';\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useRef,\n useState,\n type ForwardedRef,\n type MouseEvent,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport { SList, type SListProps } from '../SList';\n\nexport interface SDraggableListRenderState {\n onDragHandleMouseDown: (event: MouseEvent<HTMLDivElement>) => void;\n selected: boolean;\n}\n\nexport interface SDraggableListProps<T> extends Omit<SListProps, 'children' | 'onChange'> {\n /** 현재 순서대로 렌더링할 아이템 목록 */\n items: T[];\n /** 아이템을 식별할 안정적인 key */\n getKey: (item: T, index: number) => string;\n /** 아이템 렌더링 함수 */\n renderItem: (item: T, index: number, state: SDraggableListRenderState) => ReactNode;\n /** 드래그가 끝난 뒤 변경된 순서 */\n onChange: (items: T[]) => void;\n /** 외부에서 제어하는 selected 아이템 key */\n selectedKey?: string;\n /** 초기 selected 아이템 key */\n defaultSelectedKey?: string;\n /** selected 아이템이 변경될 때 호출 */\n onSelectedKeyChange?: (key: string) => void;\n /** disabled 아이템은 선택 및 드래그에서 제외 */\n getDisabled?: (item: T, index: number) => boolean;\n}\n\nfunction reorder<T>(items: T[], fromIndex: number, toIndex: number) {\n if (fromIndex === toIndex) return items;\n const nextItems = [...items];\n const [movedItem] = nextItems.splice(fromIndex, 1);\n nextItems.splice(toIndex, 0, movedItem);\n return nextItems;\n}\n\nfunction SDraggableListInner<T>(\n {\n items,\n getKey,\n renderItem,\n onChange,\n selectedKey,\n defaultSelectedKey,\n onSelectedKeyChange,\n getDisabled,\n ...rest\n }: SDraggableListProps<T>,\n ref: ForwardedRef<HTMLDivElement>,\n) {\n const [draggingIndex, setDraggingIndex] = useState<number | null>(null);\n const [dropIndex, setDropIndex] = useState<number | null>(null);\n const [internalSelectedKey, setInternalSelectedKey] = useState<string | undefined>(\n defaultSelectedKey,\n );\n const listRef = useRef<HTMLDivElement>(null);\n const fromIndexRef = useRef<number | null>(null);\n const currentSelectedKey = selectedKey ?? internalSelectedKey;\n\n const startDrag = useCallback((event: MouseEvent<HTMLDivElement>, index: number) => {\n event.preventDefault();\n fromIndexRef.current = index;\n setDraggingIndex(index);\n setDropIndex(index);\n }, []);\n\n useEffect(() => {\n if (draggingIndex == null) return undefined;\n\n const handleMouseMove = (event: globalThis.MouseEvent) => {\n const itemElements = listRef.current?.querySelectorAll<HTMLElement>(\n '[data-draggable-list-item]',\n );\n if (!itemElements) return;\n\n let nextDropIndex = items.length;\n itemElements.forEach((itemElement, index) => {\n const rect = itemElement.getBoundingClientRect();\n if (event.clientY < rect.top + rect.height / 2 && nextDropIndex === items.length) {\n nextDropIndex = index;\n }\n });\n setDropIndex(nextDropIndex);\n };\n\n const handleMouseUp = () => {\n const fromIndex = fromIndexRef.current;\n const nextDropIndex = dropIndex;\n if (fromIndex != null && nextDropIndex != null) {\n const toIndex = nextDropIndex > fromIndex ? nextDropIndex - 1 : nextDropIndex;\n if (fromIndex !== toIndex) onChange(reorder(items, fromIndex, toIndex));\n }\n fromIndexRef.current = null;\n setDraggingIndex(null);\n setDropIndex(null);\n };\n\n window.addEventListener('mousemove', handleMouseMove);\n window.addEventListener('mouseup', handleMouseUp);\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n window.removeEventListener('mouseup', handleMouseUp);\n };\n }, [draggingIndex, dropIndex, items, onChange]);\n\n return (\n <SList\n ref={node => {\n listRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n }}\n {...rest}\n >\n {items.map((item, index) => {\n const itemKey = getKey(item, index);\n const disabled = getDisabled?.(item, index) ?? false;\n const showBefore = draggingIndex != null && dropIndex === index;\n const showAfter =\n draggingIndex != null && dropIndex === items.length && index === items.length - 1;\n return (\n <div\n key={itemKey}\n data-draggable-list-item\n aria-disabled={disabled || undefined}\n onClick={() => {\n if (disabled) return;\n setInternalSelectedKey(itemKey);\n onSelectedKeyChange?.(itemKey);\n }}\n className={`relative ${\n showBefore\n ? \"before:absolute before:left-0 before:right-0 before:top-0 before:z-10 before:h-[2px] before:bg-(--sys-color-border-accent) before:content-['']\"\n : ''\n } ${\n showAfter\n ? \"after:absolute after:bottom-0 after:left-0 after:right-0 after:z-10 after:h-[2px] after:bg-(--sys-color-border-accent) after:content-['']\"\n : ''\n }`}\n >\n {renderItem(item, index, {\n onDragHandleMouseDown: event => {\n if (!disabled) startDrag(event, index);\n },\n selected: currentSelectedKey === itemKey,\n })}\n </div>\n );\n })}\n </SList>\n );\n}\n\nexport const SDraggableList = forwardRef(SDraggableListInner) as <T>(\n props: SDraggableListProps<T> & { ref?: React.ForwardedRef<HTMLDivElement> },\n) => ReactElement | null;\n"]}
@@ -0,0 +1,27 @@
1
+ import { type MouseEvent, type ReactElement, type ReactNode } from 'react';
2
+ import { type SListProps } from '../SList';
3
+ export interface SDraggableListRenderState {
4
+ onDragHandleMouseDown: (event: MouseEvent<HTMLDivElement>) => void;
5
+ selected: boolean;
6
+ }
7
+ export interface SDraggableListProps<T> extends Omit<SListProps, 'children' | 'onChange'> {
8
+ /** 현재 순서대로 렌더링할 아이템 목록 */
9
+ items: T[];
10
+ /** 아이템을 식별할 안정적인 key */
11
+ getKey: (item: T, index: number) => string;
12
+ /** 아이템 렌더링 함수 */
13
+ renderItem: (item: T, index: number, state: SDraggableListRenderState) => ReactNode;
14
+ /** 드래그가 끝난 뒤 변경된 순서 */
15
+ onChange: (items: T[]) => void;
16
+ /** 외부에서 제어하는 selected 아이템 key */
17
+ selectedKey?: string;
18
+ /** 초기 selected 아이템 key */
19
+ defaultSelectedKey?: string;
20
+ /** selected 아이템이 변경될 때 호출 */
21
+ onSelectedKeyChange?: (key: string) => void;
22
+ /** disabled 아이템은 선택 및 드래그에서 제외 */
23
+ getDisabled?: (item: T, index: number) => boolean;
24
+ }
25
+ export declare const SDraggableList: <T>(props: SDraggableListProps<T> & {
26
+ ref?: React.ForwardedRef<HTMLDivElement>;
27
+ }) => ReactElement | null;
@@ -0,0 +1,141 @@
1
+ "use client";
2
+ import { forwardRef, useState, useRef, useCallback, useEffect } from 'react';
3
+ import { clsx } from 'clsx';
4
+ import { extendTailwindMerge } from 'tailwind-merge';
5
+ import { jsx } from 'react/jsx-runtime';
6
+
7
+ var twMerge = extendTailwindMerge({
8
+ extend: {
9
+ classGroups: {
10
+ "font-size": [{ text: ["10", "11", "12", "13", "14", "16", "18", "20", "24", "28", "32"] }]
11
+ }
12
+ }
13
+ });
14
+ function cn(...inputs) {
15
+ return twMerge(clsx(inputs));
16
+ }
17
+ var SList = forwardRef(function SList2({ className, children, useGap = false, usePadding = false, ...rest }, ref) {
18
+ return /* @__PURE__ */ jsx(
19
+ "div",
20
+ {
21
+ ref,
22
+ className: cn(
23
+ useGap && "flex flex-col gap-(--cmp-list-group-gap)",
24
+ usePadding && "p-(--cmp-list-group-padding)",
25
+ className
26
+ ),
27
+ ...rest,
28
+ children
29
+ }
30
+ );
31
+ });
32
+ SList.displayName = "SList";
33
+ function reorder(items, fromIndex, toIndex) {
34
+ if (fromIndex === toIndex) return items;
35
+ const nextItems = [...items];
36
+ const [movedItem] = nextItems.splice(fromIndex, 1);
37
+ nextItems.splice(toIndex, 0, movedItem);
38
+ return nextItems;
39
+ }
40
+ function SDraggableListInner({
41
+ items,
42
+ getKey,
43
+ renderItem,
44
+ onChange,
45
+ selectedKey,
46
+ defaultSelectedKey,
47
+ onSelectedKeyChange,
48
+ getDisabled,
49
+ ...rest
50
+ }, ref) {
51
+ const [draggingIndex, setDraggingIndex] = useState(null);
52
+ const [dropIndex, setDropIndex] = useState(null);
53
+ const [internalSelectedKey, setInternalSelectedKey] = useState(
54
+ defaultSelectedKey
55
+ );
56
+ const listRef = useRef(null);
57
+ const fromIndexRef = useRef(null);
58
+ const currentSelectedKey = selectedKey ?? internalSelectedKey;
59
+ const startDrag = useCallback((event, index) => {
60
+ event.preventDefault();
61
+ fromIndexRef.current = index;
62
+ setDraggingIndex(index);
63
+ setDropIndex(index);
64
+ }, []);
65
+ useEffect(() => {
66
+ if (draggingIndex == null) return void 0;
67
+ const handleMouseMove = (event) => {
68
+ const itemElements = listRef.current?.querySelectorAll(
69
+ "[data-draggable-list-item]"
70
+ );
71
+ if (!itemElements) return;
72
+ let nextDropIndex = items.length;
73
+ itemElements.forEach((itemElement, index) => {
74
+ const rect = itemElement.getBoundingClientRect();
75
+ if (event.clientY < rect.top + rect.height / 2 && nextDropIndex === items.length) {
76
+ nextDropIndex = index;
77
+ }
78
+ });
79
+ setDropIndex(nextDropIndex);
80
+ };
81
+ const handleMouseUp = () => {
82
+ const fromIndex = fromIndexRef.current;
83
+ const nextDropIndex = dropIndex;
84
+ if (fromIndex != null && nextDropIndex != null) {
85
+ const toIndex = nextDropIndex > fromIndex ? nextDropIndex - 1 : nextDropIndex;
86
+ if (fromIndex !== toIndex) onChange(reorder(items, fromIndex, toIndex));
87
+ }
88
+ fromIndexRef.current = null;
89
+ setDraggingIndex(null);
90
+ setDropIndex(null);
91
+ };
92
+ window.addEventListener("mousemove", handleMouseMove);
93
+ window.addEventListener("mouseup", handleMouseUp);
94
+ return () => {
95
+ window.removeEventListener("mousemove", handleMouseMove);
96
+ window.removeEventListener("mouseup", handleMouseUp);
97
+ };
98
+ }, [draggingIndex, dropIndex, items, onChange]);
99
+ return /* @__PURE__ */ jsx(
100
+ SList,
101
+ {
102
+ ref: (node) => {
103
+ listRef.current = node;
104
+ if (typeof ref === "function") ref(node);
105
+ else if (ref) ref.current = node;
106
+ },
107
+ ...rest,
108
+ children: items.map((item, index) => {
109
+ const itemKey = getKey(item, index);
110
+ const disabled = getDisabled?.(item, index) ?? false;
111
+ const showBefore = draggingIndex != null && dropIndex === index;
112
+ const showAfter = draggingIndex != null && dropIndex === items.length && index === items.length - 1;
113
+ return /* @__PURE__ */ jsx(
114
+ "div",
115
+ {
116
+ "data-draggable-list-item": true,
117
+ "aria-disabled": disabled || void 0,
118
+ onClick: () => {
119
+ if (disabled) return;
120
+ setInternalSelectedKey(itemKey);
121
+ onSelectedKeyChange?.(itemKey);
122
+ },
123
+ className: `relative ${showBefore ? "before:absolute before:left-0 before:right-0 before:top-0 before:z-10 before:h-[2px] before:bg-(--sys-color-border-accent) before:content-['']" : ""} ${showAfter ? "after:absolute after:bottom-0 after:left-0 after:right-0 after:z-10 after:h-[2px] after:bg-(--sys-color-border-accent) after:content-['']" : ""}`,
124
+ children: renderItem(item, index, {
125
+ onDragHandleMouseDown: (event) => {
126
+ if (!disabled) startDrag(event, index);
127
+ },
128
+ selected: currentSelectedKey === itemKey
129
+ })
130
+ },
131
+ itemKey
132
+ );
133
+ })
134
+ }
135
+ );
136
+ }
137
+ var SDraggableList = forwardRef(SDraggableListInner);
138
+
139
+ export { SDraggableList };
140
+ //# sourceMappingURL=SDraggableList.js.map
141
+ //# sourceMappingURL=SDraggableList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/cn.ts","../../../src/components/SList/SList.tsx","../../../src/components/SDraggableList/SDraggableList.tsx"],"names":["SList","jsx","forwardRef"],"mappings":"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","file":"SDraggableList.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\n/**\n * theme.css 의 --text-* 는 기본 Tailwind 폰트사이즈 스케일(xs/sm/base...)이 아닌\n * 숫자 스케일(10~32)이라, 확장 없이는 tailwind-merge 가 이를 텍스트 컬러 그룹으로\n * 오인해 text-12 같은 클래스를 text-(--color-var) 뒤에서 충돌 제거해버린다.\n */\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [{ text: ['10', '11', '12', '13', '14', '16', '18', '20', '24', '28', '32'] }],\n },\n },\n});\n\n/**\n * className 병합 유틸.\n * clsx로 조건부 클래스를 합치고, tailwind-merge로 Tailwind 클래스 충돌을 해소한다.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport { forwardRef, type HTMLAttributes, type ReactNode } from 'react';\nimport { cn } from '../../lib/cn';\n\nexport interface SListProps extends HTMLAttributes<HTMLDivElement> {\n /** 리스트 컨테이너 내부에 렌더링할 내용 */\n children?: ReactNode;\n /** 리스트 아이템 사이 gap 토큰 적용 여부 */\n useGap?: boolean;\n /** 리스트 컨테이너 padding 토큰 적용 여부 */\n usePadding?: boolean;\n}\n\nexport const SList = forwardRef<HTMLDivElement, SListProps>(function SList(\n { className, children, useGap = false, usePadding = false, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cn(\n useGap && 'flex flex-col gap-(--cmp-list-group-gap)',\n usePadding && 'p-(--cmp-list-group-padding)',\n className,\n )}\n {...rest}\n >\n {children}\n </div>\n );\n});\n\nSList.displayName = 'SList';\n","'use client';\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useRef,\n useState,\n type ForwardedRef,\n type MouseEvent,\n type ReactElement,\n type ReactNode,\n} from 'react';\nimport { SList, type SListProps } from '../SList';\n\nexport interface SDraggableListRenderState {\n onDragHandleMouseDown: (event: MouseEvent<HTMLDivElement>) => void;\n selected: boolean;\n}\n\nexport interface SDraggableListProps<T> extends Omit<SListProps, 'children' | 'onChange'> {\n /** 현재 순서대로 렌더링할 아이템 목록 */\n items: T[];\n /** 아이템을 식별할 안정적인 key */\n getKey: (item: T, index: number) => string;\n /** 아이템 렌더링 함수 */\n renderItem: (item: T, index: number, state: SDraggableListRenderState) => ReactNode;\n /** 드래그가 끝난 뒤 변경된 순서 */\n onChange: (items: T[]) => void;\n /** 외부에서 제어하는 selected 아이템 key */\n selectedKey?: string;\n /** 초기 selected 아이템 key */\n defaultSelectedKey?: string;\n /** selected 아이템이 변경될 때 호출 */\n onSelectedKeyChange?: (key: string) => void;\n /** disabled 아이템은 선택 및 드래그에서 제외 */\n getDisabled?: (item: T, index: number) => boolean;\n}\n\nfunction reorder<T>(items: T[], fromIndex: number, toIndex: number) {\n if (fromIndex === toIndex) return items;\n const nextItems = [...items];\n const [movedItem] = nextItems.splice(fromIndex, 1);\n nextItems.splice(toIndex, 0, movedItem);\n return nextItems;\n}\n\nfunction SDraggableListInner<T>(\n {\n items,\n getKey,\n renderItem,\n onChange,\n selectedKey,\n defaultSelectedKey,\n onSelectedKeyChange,\n getDisabled,\n ...rest\n }: SDraggableListProps<T>,\n ref: ForwardedRef<HTMLDivElement>,\n) {\n const [draggingIndex, setDraggingIndex] = useState<number | null>(null);\n const [dropIndex, setDropIndex] = useState<number | null>(null);\n const [internalSelectedKey, setInternalSelectedKey] = useState<string | undefined>(\n defaultSelectedKey,\n );\n const listRef = useRef<HTMLDivElement>(null);\n const fromIndexRef = useRef<number | null>(null);\n const currentSelectedKey = selectedKey ?? internalSelectedKey;\n\n const startDrag = useCallback((event: MouseEvent<HTMLDivElement>, index: number) => {\n event.preventDefault();\n fromIndexRef.current = index;\n setDraggingIndex(index);\n setDropIndex(index);\n }, []);\n\n useEffect(() => {\n if (draggingIndex == null) return undefined;\n\n const handleMouseMove = (event: globalThis.MouseEvent) => {\n const itemElements = listRef.current?.querySelectorAll<HTMLElement>(\n '[data-draggable-list-item]',\n );\n if (!itemElements) return;\n\n let nextDropIndex = items.length;\n itemElements.forEach((itemElement, index) => {\n const rect = itemElement.getBoundingClientRect();\n if (event.clientY < rect.top + rect.height / 2 && nextDropIndex === items.length) {\n nextDropIndex = index;\n }\n });\n setDropIndex(nextDropIndex);\n };\n\n const handleMouseUp = () => {\n const fromIndex = fromIndexRef.current;\n const nextDropIndex = dropIndex;\n if (fromIndex != null && nextDropIndex != null) {\n const toIndex = nextDropIndex > fromIndex ? nextDropIndex - 1 : nextDropIndex;\n if (fromIndex !== toIndex) onChange(reorder(items, fromIndex, toIndex));\n }\n fromIndexRef.current = null;\n setDraggingIndex(null);\n setDropIndex(null);\n };\n\n window.addEventListener('mousemove', handleMouseMove);\n window.addEventListener('mouseup', handleMouseUp);\n return () => {\n window.removeEventListener('mousemove', handleMouseMove);\n window.removeEventListener('mouseup', handleMouseUp);\n };\n }, [draggingIndex, dropIndex, items, onChange]);\n\n return (\n <SList\n ref={node => {\n listRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n }}\n {...rest}\n >\n {items.map((item, index) => {\n const itemKey = getKey(item, index);\n const disabled = getDisabled?.(item, index) ?? false;\n const showBefore = draggingIndex != null && dropIndex === index;\n const showAfter =\n draggingIndex != null && dropIndex === items.length && index === items.length - 1;\n return (\n <div\n key={itemKey}\n data-draggable-list-item\n aria-disabled={disabled || undefined}\n onClick={() => {\n if (disabled) return;\n setInternalSelectedKey(itemKey);\n onSelectedKeyChange?.(itemKey);\n }}\n className={`relative ${\n showBefore\n ? \"before:absolute before:left-0 before:right-0 before:top-0 before:z-10 before:h-[2px] before:bg-(--sys-color-border-accent) before:content-['']\"\n : ''\n } ${\n showAfter\n ? \"after:absolute after:bottom-0 after:left-0 after:right-0 after:z-10 after:h-[2px] after:bg-(--sys-color-border-accent) after:content-['']\"\n : ''\n }`}\n >\n {renderItem(item, index, {\n onDragHandleMouseDown: event => {\n if (!disabled) startDrag(event, index);\n },\n selected: currentSelectedKey === itemKey,\n })}\n </div>\n );\n })}\n </SList>\n );\n}\n\nexport const SDraggableList = forwardRef(SDraggableListInner) as <T>(\n props: SDraggableListProps<T> & { ref?: React.ForwardedRef<HTMLDivElement> },\n) => ReactElement | null;\n"]}
@@ -0,0 +1 @@
1
+ export { SDraggableList, type SDraggableListProps, type SDraggableListRenderState, } from './SDraggableList';
@@ -16,6 +16,9 @@
16
16
  | `bordered?` | `boolean` | `false` | 외곽 테두리 사용 여부 |
17
17
  | `expanded?` | `boolean` | — | |
18
18
  | `defaultExpanded?` | `boolean` | `false` | |
19
+ | `selected?` | `boolean` | `false` | |
20
+ | `interaction?` | `SExpansionItemInteraction` | — | hover/selected 상태에서 적용할 인터랙션 preset |
21
+ | `accentStripe?` | `boolean` | `false` | 아이템 왼쪽 accent stripe 표시 여부 |
19
22
  | `dense?` | `boolean` | `false` | |
20
23
  | `disabled?` | `boolean` | `false` | |
21
24
 
@@ -27,6 +30,10 @@
27
30
 
28
31
  ## Dependencies
29
32
 
33
+ ### Used by
34
+
35
+ - [SExpansionList](../SExpansionList)
36
+
30
37
  ### Depends on
31
38
 
32
39
  - [SIcon](../SIcon)
@@ -36,5 +43,6 @@
36
43
  ```mermaid
37
44
  graph TD;
38
45
  SExpansionItem --> SIcon
46
+ SExpansionList --> SExpansionItem
39
47
  style SExpansionItem fill:#f9f,stroke:#333,stroke-width:4px
40
48
  ```
@@ -2851,6 +2851,9 @@ var SIcon = react.forwardRef(function SIcon2({ name, size = 24, color, rotate, l
2851
2851
  }
2852
2852
  );
2853
2853
  });
2854
+ var SExpansionItemDescendantContext = react.createContext(
2855
+ null
2856
+ );
2854
2857
  var SExpansionItem = react.forwardRef(
2855
2858
  function SExpansionItem2({
2856
2859
  title,
@@ -2861,6 +2864,9 @@ var SExpansionItem = react.forwardRef(
2861
2864
  bordered = false,
2862
2865
  expanded,
2863
2866
  defaultExpanded = false,
2867
+ selected = false,
2868
+ interaction,
2869
+ accentStripe = false,
2864
2870
  dense = false,
2865
2871
  disabled = false,
2866
2872
  onToggle,
@@ -2869,15 +2875,39 @@ var SExpansionItem = react.forwardRef(
2869
2875
  ...rest
2870
2876
  }, ref) {
2871
2877
  const [internalExpanded, setInternalExpanded] = react.useState(defaultExpanded);
2878
+ const [expandedDescendantCount, setExpandedDescendantCount] = react.useState(0);
2879
+ const [hovered, setHovered] = react.useState(false);
2880
+ const parentDescendantContext = react.useContext(SExpansionItemDescendantContext);
2872
2881
  const isExpanded = expanded ?? internalExpanded;
2873
- const leadingNode = typeof leading === "function" ? leading({ expanded: isExpanded, disabled }) : leading;
2882
+ const isSelected = isExpanded && (selected || expandedDescendantCount === 0);
2883
+ const hasChevronInteraction = interaction === "chevron";
2884
+ const showInteractionChevron = hasChevronInteraction && !disabled && (isSelected || hovered);
2885
+ const interactionChevronColor = isSelected ? "var(--sys-color-fg-brand)" : "var(--sys-color-fg-quaternary)";
2886
+ const reportExpandedDescendant = react.useCallback(
2887
+ (delta) => {
2888
+ setExpandedDescendantCount((current) => Math.max(0, current + delta));
2889
+ parentDescendantContext?.reportExpandedDescendant(delta);
2890
+ },
2891
+ [parentDescendantContext]
2892
+ );
2893
+ const descendantContextValue = react.useMemo(
2894
+ () => ({ reportExpandedDescendant }),
2895
+ [reportExpandedDescendant]
2896
+ );
2897
+ const leadingNode = typeof leading === "function" ? leading({ expanded: isExpanded, selected: isSelected, disabled }) : leading;
2874
2898
  const id = react.useId();
2899
+ react.useEffect(() => {
2900
+ if (!isExpanded) return;
2901
+ parentDescendantContext?.reportExpandedDescendant(1);
2902
+ return () => parentDescendantContext?.reportExpandedDescendant(-1);
2903
+ }, [isExpanded, parentDescendantContext]);
2875
2904
  return /* @__PURE__ */ jsxRuntime.jsxs(
2876
2905
  "div",
2877
2906
  {
2878
2907
  ref,
2879
2908
  className: cn(
2880
- "group/expansion-item overflow-hidden bg-(--cmp-listExpansionItem-title-bg-default)",
2909
+ "group/expansion-item relative overflow-hidden bg-(--cmp-listExpansionItem-title-bg-default)",
2910
+ !bordered && "border-b border-(--cmp-listExpansionItem-border-default)",
2881
2911
  // TODO: expansionItem/bordered/radius 및 border intent 토큰 생성 후 고정값을 토큰으로 교체한다.
2882
2912
  bordered && "rounded-[8px] border border-[#E1E1E1] hover:border-[#AAAAAA] [&:has(>button:hover)]:border-[#AAAAAA]",
2883
2913
  bordered && isExpanded && "!border-[#93C4FF]",
@@ -2885,6 +2915,13 @@ var SExpansionItem = react.forwardRef(
2885
2915
  ),
2886
2916
  ...rest,
2887
2917
  children: [
2918
+ accentStripe && !bordered && /* @__PURE__ */ jsxRuntime.jsx(
2919
+ "span",
2920
+ {
2921
+ "aria-hidden": "true",
2922
+ className: "pointer-events-none absolute bottom-[-1px] left-0 top-0 z-[1] w-(--cmp-listItem-accentStripe-width) bg-(--cmp-listItem-accentStripe-color)"
2923
+ }
2924
+ ),
2888
2925
  /* @__PURE__ */ jsxRuntime.jsxs(
2889
2926
  "button",
2890
2927
  {
@@ -2892,6 +2929,8 @@ var SExpansionItem = react.forwardRef(
2892
2929
  disabled,
2893
2930
  "aria-expanded": isExpanded,
2894
2931
  "aria-controls": id,
2932
+ onMouseEnter: () => setHovered(true),
2933
+ onMouseLeave: () => setHovered(false),
2895
2934
  onClick: (event) => {
2896
2935
  if (disabled) return;
2897
2936
  const next = !isExpanded;
@@ -2899,14 +2938,17 @@ var SExpansionItem = react.forwardRef(
2899
2938
  onToggle?.(next, event);
2900
2939
  },
2901
2940
  className: cn(
2902
- "flex w-full cursor-pointer items-center justify-between border-0 text-left text-[14px] font-bold leading-[24px] text-(--cmp-listExpansionItem-title-text-default) focus-visible:outline focus-visible:outline-2 focus-visible:outline-(--sys-color-border-accent) disabled:cursor-not-allowed disabled:text-(--sys-color-disabled-text)",
2941
+ "group/expansion-item-button flex w-full cursor-pointer items-center justify-between border-0 text-left text-[14px] font-bold leading-[24px] text-(--cmp-listExpansionItem-title-text-default) focus-visible:outline focus-visible:outline-2 focus-visible:outline-(--sys-color-border-accent) disabled:cursor-not-allowed disabled:text-(--sys-color-disabled-text)",
2903
2942
  isExpanded && "border-b border-(--cmp-listExpansionItem-title-border)",
2904
- !isExpanded && "hover:bg-(--cmp-listExpansionItem-title-bg-hover)",
2943
+ hasChevronInteraction ? "hover:bg-(--sys-color-bg-neutralBright)" : !isExpanded && "hover:bg-(--cmp-listExpansionItem-title-bg-hover)",
2905
2944
  dense ? "h-[32px] py-(--cmp-listExpansionItem-title-paddingY-dense)" : "h-[40px] py-(--cmp-listExpansionItem-title-paddingY-normal)",
2906
2945
  "gap-(--cmp-listExpansionItem-title-gap)",
2907
2946
  "pr-(--cmp-listExpansionItem-title-paddingX)",
2908
- depth === 2 ? "pl-[calc(var(--cmp-listExpansionItem-title-paddingX)+32px)]" : depth === 3 ? "pl-[calc(var(--cmp-listExpansionItem-title-paddingX)+64px)]" : "pl-(--cmp-listExpansionItem-title-paddingX)",
2909
- isExpanded && "bg-(--cmp-listExpansionItem-title-bg-expanded) text-(--cmp-listExpansionItem-title-text-expanded)"
2947
+ depth === 2 ? "pl-[36px]" : depth === 3 ? "pl-[56px]" : "pl-[16px]",
2948
+ isExpanded && cn(
2949
+ "text-(--cmp-listExpansionItem-title-text-expanded)",
2950
+ isSelected && "bg-(--cmp-listExpansionItem-title-bg-expanded)"
2951
+ )
2910
2952
  ),
2911
2953
  children: [
2912
2954
  !leading && /* @__PURE__ */ jsxRuntime.jsx(
@@ -2921,11 +2963,12 @@ var SExpansionItem = react.forwardRef(
2921
2963
  ),
2922
2964
  leadingNode,
2923
2965
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex min-w-0 flex-1 flex-col", children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: title }) }),
2924
- trailing
2966
+ trailing,
2967
+ showInteractionChevron && /* @__PURE__ */ jsxRuntime.jsx(SIcon, { name: "chevronRight", size: 12, color: interactionChevronColor })
2925
2968
  ]
2926
2969
  }
2927
2970
  ),
2928
- /* @__PURE__ */ jsxRuntime.jsx(
2971
+ /* @__PURE__ */ jsxRuntime.jsx(SExpansionItemDescendantContext.Provider, { value: descendantContextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
2929
2972
  "div",
2930
2973
  {
2931
2974
  id,
@@ -2936,7 +2979,7 @@ var SExpansionItem = react.forwardRef(
2936
2979
  ),
2937
2980
  children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(bodyPadding && "p-(--cmp-listExpansionItem-body-padding)"), children }) })
2938
2981
  }
2939
- )
2982
+ ) })
2940
2983
  ]
2941
2984
  }
2942
2985
  );