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.
- package/dist/components/SConfirmModal/README.md +1 -0
- package/dist/components/SConfirmModal/SConfirmModal.cjs +3 -1
- package/dist/components/SConfirmModal/SConfirmModal.cjs.map +1 -1
- package/dist/components/SConfirmModal/SConfirmModal.d.ts +3 -1
- package/dist/components/SConfirmModal/SConfirmModal.js +3 -1
- package/dist/components/SConfirmModal/SConfirmModal.js.map +1 -1
- package/dist/components/SDraggableItem/README.md +1 -0
- package/dist/components/SDraggableItem/SDraggableItem.cjs +2 -1
- package/dist/components/SDraggableItem/SDraggableItem.cjs.map +1 -1
- package/dist/components/SDraggableItem/SDraggableItem.d.ts +2 -0
- package/dist/components/SDraggableItem/SDraggableItem.js +2 -1
- package/dist/components/SDraggableItem/SDraggableItem.js.map +1 -1
- package/dist/components/SDraggableList/README.md +37 -0
- package/dist/components/SDraggableList/SDraggableList.cjs +143 -0
- package/dist/components/SDraggableList/SDraggableList.cjs.map +1 -0
- package/dist/components/SDraggableList/SDraggableList.d.ts +27 -0
- package/dist/components/SDraggableList/SDraggableList.js +141 -0
- package/dist/components/SDraggableList/SDraggableList.js.map +1 -0
- package/dist/components/SDraggableList/index.d.ts +1 -0
- package/dist/components/SExpansionItem/README.md +8 -0
- package/dist/components/SExpansionItem/SExpansionItem.cjs +52 -9
- package/dist/components/SExpansionItem/SExpansionItem.cjs.map +1 -1
- package/dist/components/SExpansionItem/SExpansionItem.d.ts +7 -0
- package/dist/components/SExpansionItem/SExpansionItem.js +53 -10
- package/dist/components/SExpansionItem/SExpansionItem.js.map +1 -1
- package/dist/components/SExpansionList/README.md +9 -0
- package/dist/components/SExpansionList/SExpansionList.cjs +3150 -0
- package/dist/components/SExpansionList/SExpansionList.cjs.map +1 -0
- package/dist/components/SExpansionList/SExpansionList.d.ts +3 -0
- package/dist/components/SExpansionList/SExpansionList.js +3148 -0
- package/dist/components/SExpansionList/SExpansionList.js.map +1 -0
- package/dist/components/SExpansionList/index.d.ts +1 -0
- package/dist/components/SList/README.md +15 -0
- package/dist/components/SListItem/README.md +21 -0
- package/dist/components/SListItem/SListItem.cjs +2858 -5
- package/dist/components/SListItem/SListItem.cjs.map +1 -1
- package/dist/components/SListItem/SListItem.d.ts +7 -0
- package/dist/components/SListItem/SListItem.js +2859 -6
- package/dist/components/SListItem/SListItem.js.map +1 -1
- package/dist/components/SModal/README.md +1 -1
- package/dist/components/SModal/SModal.cjs +3 -1
- package/dist/components/SModal/SModal.cjs.map +1 -1
- package/dist/components/SModal/SModal.js +3 -1
- package/dist/components/SModal/SModal.js.map +1 -1
- package/dist/components/SSectionHeaderCard/README.md +1 -0
- package/dist/components/SSectionHeaderCard/SSectionHeaderCard.cjs +15 -1
- package/dist/components/SSectionHeaderCard/SSectionHeaderCard.cjs.map +1 -1
- package/dist/components/SSectionHeaderCard/SSectionHeaderCard.d.ts +3 -0
- package/dist/components/SSectionHeaderCard/SSectionHeaderCard.js +15 -1
- package/dist/components/SSectionHeaderCard/SSectionHeaderCard.js.map +1 -1
- package/dist/components/SSectionHeaderCard/index.d.ts +1 -1
- package/dist/index.cjs +282 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +282 -35
- package/dist/index.js.map +1 -1
- package/dist/llms-full.txt +81 -1
- package/dist/llms.txt +2 -2
- package/dist/styles.css +59 -63
- 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
|
|
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-[
|
|
2909
|
-
isExpanded &&
|
|
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
|
);
|