@loword/loword-design-system 0.5.0 → 0.5.1
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/LDButton/LDButton.d.ts +2 -0
- package/dist/components/LDButton/LDButton.d.ts.map +1 -1
- package/dist/components/LDButton/LDButton.js +18 -3
- package/dist/components/LDDrawer/LDDrawer.d.ts.map +1 -1
- package/dist/components/LDDrawer/LDDrawer.js +10 -9
- package/dist/components/LDEditor/EditorBase.d.ts.map +1 -1
- package/dist/components/LDEditor/EditorBase.js +7 -1
- package/dist/components/LDEditor/LDEditor.d.ts +8 -1
- package/dist/components/LDEditor/LDEditor.d.ts.map +1 -1
- package/dist/components/LDEditor/LDEditor.js +340 -100
- package/dist/components/LDModal/LDModal.d.ts +2 -0
- package/dist/components/LDModal/LDModal.d.ts.map +1 -1
- package/dist/components/LDModal/LDModal.js +1 -1
- package/dist/components/LDModal/LDModalView.d.ts +3 -1
- package/dist/components/LDModal/LDModalView.d.ts.map +1 -1
- package/dist/components/LDModal/LDModalView.js +3 -3
- package/dist/index.css +84 -45
- package/package.json +1 -1
- package/src/styles/components/editor.css +12 -2
|
@@ -15,6 +15,8 @@ type _ButtonProps = {
|
|
|
15
15
|
shape?: ButtonShape;
|
|
16
16
|
align?: ButtonAlign;
|
|
17
17
|
loading?: boolean;
|
|
18
|
+
/** 비동기 클릭 핸들러. 실행이 끝날 때까지 자동으로 loading·disabled 상태가 되어 연속 클릭이 막힌다 */
|
|
19
|
+
asyncClick?: (e: React.MouseEvent<HTMLElement>) => Promise<unknown>;
|
|
18
20
|
block?: boolean;
|
|
19
21
|
leftIcon?: React.ReactNode;
|
|
20
22
|
rightIcon?: React.ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LDButton.d.ts","sourceRoot":"","sources":["../../../src/components/LDButton/LDButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"LDButton.d.ts","sourceRoot":"","sources":["../../../src/components/LDButton/LDButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,KAAK,WAAW,EAAY,MAAM,OAAO,CAAC;AAMnD,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAM7C,KAAK,aAAa,GAAG,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC;AAC7E,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;AACxE,MAAM,MAAM,UAAU,GAAG,MAAM,CAC7B,IAAI,EACJ,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAC/D,CAAC;AACF,KAAK,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AACzC,KAAK,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAgO/C,KAAK,YAAY,GAAG;IAClB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,WAAW,IAAI,yBAAyB,CAC1E,CAAC,EACD,YAAY,CACb,CAAC;AAEF,KAAK,eAAe,GAAG,CAAC,CAAC,SAAS,KAAK,CAAC,WAAW,GAAG,QAAQ,EAC5D,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KACpB,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;AAE/B,QAAA,MAAM,QAAQ,EAAE,eA0Ef,CAAC;AAEF,eAAe,QAAQ,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import ButtonBase from '../../components/LDButton/ButtonBase.js';
|
|
3
4
|
import LDSpinner from '../../components/LDSpinner/LDSpinner.js';
|
|
4
5
|
import RenderIcon from '../../components/RenderIcon/RenderIcon.js';
|
|
@@ -210,7 +211,21 @@ const buttonVariants = tv({
|
|
|
210
211
|
},
|
|
211
212
|
],
|
|
212
213
|
});
|
|
213
|
-
const LDButton = ({ as, variant = 'solid', color = 'point', size = 'small', shape = 'default', align = 'center', loading = false, block = false, children, className, leftIcon, rightIcon, compact = false, ref, ...props }) => {
|
|
214
|
+
const LDButton = ({ as, variant = 'solid', color = 'point', size = 'small', shape = 'default', align = 'center', loading = false, asyncClick, block = false, children, className, leftIcon, rightIcon, compact = false, ref, ...props }) => {
|
|
215
|
+
const [pending, setPending] = useState(false);
|
|
216
|
+
const isLoading = loading || pending;
|
|
217
|
+
/* as가 href를 가지면 disabled여도 span으로 렌더돼 클릭이 살아 있으므로 pending도 직접 막는다 */
|
|
218
|
+
const handleAsyncClick = async (e) => {
|
|
219
|
+
if (pending)
|
|
220
|
+
return;
|
|
221
|
+
setPending(true);
|
|
222
|
+
try {
|
|
223
|
+
await asyncClick?.(e);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
setPending(false);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
214
229
|
return (_jsxs(ButtonBase.Root, { as: as, ref: ref, className: buttonVariants({
|
|
215
230
|
variant,
|
|
216
231
|
color,
|
|
@@ -219,10 +234,10 @@ const LDButton = ({ as, variant = 'solid', color = 'point', size = 'small', shap
|
|
|
219
234
|
align,
|
|
220
235
|
block,
|
|
221
236
|
compact,
|
|
222
|
-
loading,
|
|
237
|
+
loading: isLoading,
|
|
223
238
|
// 미지정도 false 분기(기본 색·hover)를 타야 하므로 boolean으로 좁혀 넘긴다.
|
|
224
239
|
disabled: !!props.disabled,
|
|
225
240
|
className,
|
|
226
|
-
}), ...props, disabled: props.disabled ||
|
|
241
|
+
}), ...props, disabled: props.disabled || isLoading, ...(asyncClick && { onClick: handleAsyncClick }), children: [_jsx(Show, { when: !isLoading, fallback: _jsx(LDSpinner, { className: "absolute top-1/2 left-1/2 -translate-1/2" }), children: _jsx(RenderIcon, { children: leftIcon }) }), _jsx(Show, { when: children, children: _jsx("span", { className: cn('shrink-0', isLoading && 'invisible'), children: children }) }), _jsx(RenderIcon, { className: cn(isLoading && 'invisible'), children: rightIcon })] }));
|
|
227
242
|
};
|
|
228
243
|
export default LDButton;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LDDrawer.d.ts","sourceRoot":"","sources":["../../../src/components/LDDrawer/LDDrawer.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"LDDrawer.d.ts","sourceRoot":"","sources":["../../../src/components/LDDrawer/LDDrawer.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE5D,OAAiB,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,UAAU,EAAE,EACjB,KAAK,eAAe,EAErB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAI7C,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC,aAAa;IACb,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,EAAE,CAAC,CAAC;IACT,gBAAgB;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC;IACzD,eAAe;IACf,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IACvC,yCAAyC;IACzC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0BAA0B;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yBAAyB;IACzB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,cAAc;IACd,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,kDAAkD;IAClD,UAAU,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACjC,mBAAmB;IACnB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACzC,mBAAmB;IACnB,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACjE,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sBAAsB;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa;IACb,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,eAAe;IACf,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,oCAAoC;IACpC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C;AAED,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC;AACvE,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;AAC7E,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;AAC7E,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;AAC7E,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC3E,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;AACzE,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;AACzE,MAAM,MAAM,wBAAwB,GAAG,cAAc,CACnD,OAAO,UAAU,CAAC,WAAW,CAC9B,CAAC;AACF,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;AACvD,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;AACxD,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;AACtD,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;AA4HxD,mDAAmD;AACnD,QAAA,MAAM,QAAQ;oMAiBX,aAAa;kBAlIK,iBAAiB;qBAGd,oBAAoB;uCAKF,oBAAoB;uCAQpB,oBAAoB;qCAetB,kBAAkB;sCAYjB,mBAAmB;8EAiBzD,kBAAkB;sCAmBoB,mBAAmB;qCAWpB,kBAAkB;2CAKZ,wBAAwB;oCAQ/B,iBAAiB;sCAKf,mBAAmB;CAuF3D,CAAC;AAeF,eAAe,QAAQ,CAAC"}
|
|
@@ -6,10 +6,10 @@ import DrawerBase, { useDrawerContext, } from '../../components/LDDrawer/DrawerB
|
|
|
6
6
|
import cn from '../../utils/cn.js';
|
|
7
7
|
// ── 방향별 스타일 매핑 ──
|
|
8
8
|
const directionClasses = {
|
|
9
|
-
bottom: 'bottom-0 left-0 right-0 rounded-t-
|
|
10
|
-
top: 'top-0 left-0 right-0 rounded-b-
|
|
11
|
-
left: 'left-0 top-0 bottom-0 rounded-r-
|
|
12
|
-
right: 'right-0 top-0 bottom-0 rounded-l-
|
|
9
|
+
bottom: 'bottom-0 left-0 right-0 rounded-t-[10px] max-h-[95dvh] flex flex-col',
|
|
10
|
+
top: 'top-0 left-0 right-0 rounded-b-[10px] max-h-[95dvh] flex flex-col',
|
|
11
|
+
left: 'left-0 top-0 bottom-0 rounded-r-[10px] w-100 h-full flex flex-col',
|
|
12
|
+
right: 'right-0 top-0 bottom-0 rounded-l-[10px] w-100 h-full flex flex-col',
|
|
13
13
|
};
|
|
14
14
|
/** 열림 상태와 슬라이드 방향을 관리하는 Styled Compound 루트입니다. */
|
|
15
15
|
const Root = (props) => _jsx(DrawerBase.Root, { ...props });
|
|
@@ -23,13 +23,14 @@ const Content = ({ className, ...props }) => {
|
|
|
23
23
|
return (_jsx(DrawerBase.Content, { className: cn('fixed z-50 bg-white outline-hidden', directionClasses[direction], className), ...props }));
|
|
24
24
|
};
|
|
25
25
|
/** Content 안쪽의 여백과 세로 배치를 담당합니다. Handle은 이 바깥에 둡니다. */
|
|
26
|
-
const Panel = ({ className, ...props }) => (_jsx("div", { className: cn('relative flex flex-1 flex-col gap-
|
|
27
|
-
/** 드래그로 LDDrawer를 여닫는 손잡이입니다. */
|
|
28
|
-
|
|
26
|
+
const Panel = ({ className, ...props }) => (_jsx("div", { className: cn('relative flex flex-1 flex-col gap-5 overflow-hidden p-5', className), ...props }));
|
|
27
|
+
/** 드래그로 LDDrawer를 여닫는 손잡이입니다. 공간을 차지하지 않고 패널 상단에 겹쳐 뜹니다. */
|
|
28
|
+
// vaul이 [data-vaul-handle]에 unlayered CSS를 주입해 Tailwind 유틸리티를 이기므로 겹치는 속성은 `!`로 강제한다.
|
|
29
|
+
const Handle = ({ className, ...props }) => (_jsx(DrawerBase.Handle, { className: cn('absolute! top-[17px] left-1/2 z-10 h-1.5! w-19! -translate-x-1/2 rounded-[10px]! bg-neutral100! opacity-100!', className), ...props }));
|
|
29
30
|
/** LDDrawer를 닫는 버튼입니다. children 미지정 시 기본 X 아이콘을 렌더합니다. */
|
|
30
|
-
const Close = ({ children, className, type = 'button', 'aria-label': ariaLabel = '닫기', ...props }) => (_jsx(DrawerBase.Close, { type: type, "aria-label": ariaLabel, className: cn('absolute top-
|
|
31
|
+
const Close = ({ children, className, type = 'button', 'aria-label': ariaLabel = '닫기', ...props }) => (_jsx(DrawerBase.Close, { type: type, "aria-label": ariaLabel, className: cn('absolute top-5 right-5 z-10 flex cursor-pointer text-neutral500 outline-hidden transition-colors after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2 hover:text-neutral700', className), ...props, children: children === undefined ? (_jsx(X, { className: "size-4 tablet:size-5 pc:size-6" })) : (children) }));
|
|
31
32
|
/** 제목과 설명을 묶는 헤더 영역입니다. */
|
|
32
|
-
const Header = ({ className, ...props }) => (_jsx("div", { className: cn('flex flex-col gap-2
|
|
33
|
+
const Header = ({ className, ...props }) => (_jsx("div", { className: cn('flex flex-col items-center gap-2.5 py-5 text-center', className), ...props }));
|
|
33
34
|
/** LDDrawer의 제목입니다. 스크린리더가 읽는 접근 가능한 이름이 됩니다. */
|
|
34
35
|
const Title = ({ className, ...props }) => (_jsx(DrawerBase.Title, { className: cn('display20 bold', className), ...props }));
|
|
35
36
|
/** LDDrawer의 보조 설명입니다. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorBase.d.ts","sourceRoot":"","sources":["../../../src/components/LDEditor/EditorBase.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,UAAU,EAEhB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAKf,UAAU,iBAAiB;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAID,eAAO,MAAM,gBAAgB,yBAS5B,CAAC;AAGF,UAAU,SAAS;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"EditorBase.d.ts","sourceRoot":"","sources":["../../../src/components/LDEditor/EditorBase.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,UAAU,EAEhB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAKf,UAAU,iBAAiB;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAID,eAAO,MAAM,gBAAgB,yBAS5B,CAAC;AAGF,UAAU,SAAS;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AA2CD,UAAU,YAAY;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAQD,UAAU,YAAY;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAeD,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA0BD,UAAU,oBAAoB;IAC5B,OAAO,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,SAAS,CAAC;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,SAAS,CAAC,CAAC;CACrE;AA4ED,UAAU,aAAa;IACrB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgBD,eAAO,MAAM,UAAU;oEAnMpB,SAAS;6BAuCoB,YAAY;wCAWE,YAAY;yFA2BvD,kBAAkB;sDA4BlB,oBAAoB;;mDA4E+B,aAAa;CAmBlE,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
|
@@ -19,6 +19,9 @@ const Root = ({ extensions, content = '', onChange, className, children, }) => {
|
|
|
19
19
|
immediatelyRender: false,
|
|
20
20
|
editorProps: {
|
|
21
21
|
attributes: {
|
|
22
|
+
// role이 없는 div에는 aria-label이 허용되지 않아 스크린리더가 이름을 무시한다.
|
|
23
|
+
role: 'textbox',
|
|
24
|
+
'aria-multiline': 'true',
|
|
22
25
|
'aria-label': '본문 편집기',
|
|
23
26
|
class: cn('prose prose-lg h-204.5 max-w-none overflow-y-auto rounded-b-md border-x border-b border-neutral200 px-4 py-3 outline-none focus:outline-none mobile:h-[320px]', className),
|
|
24
27
|
},
|
|
@@ -86,7 +89,10 @@ const ToolbarDropdown = ({ trigger, className, children, }) => {
|
|
|
86
89
|
window.addEventListener('resize', measure);
|
|
87
90
|
return () => window.removeEventListener('resize', measure);
|
|
88
91
|
}, [open]);
|
|
89
|
-
return (
|
|
92
|
+
return (
|
|
93
|
+
// flex가 없으면 트리거가 inline 박스가 되어 baseline 아래 여백만큼 래퍼가
|
|
94
|
+
// 버튼보다 높아진다. 그만큼 툴바에서 다른 버튼과 세로 위치가 어긋난다.
|
|
95
|
+
_jsxs("div", { ref: ref, className: "relative flex", children: [trigger({ open, toggle }), open && (_jsx("div", { ref: panelRef, style: { left: -shift }, className: cn('absolute top-full z-50 mt-1', className), children: typeof children === 'function' ? children({ close }) : children }))] }));
|
|
90
96
|
};
|
|
91
97
|
const Toolbar = Object.assign(ToolbarRoot, {
|
|
92
98
|
LDButton: ToolbarButton,
|
|
@@ -2,11 +2,18 @@ export interface LDEditorProps {
|
|
|
2
2
|
content: string;
|
|
3
3
|
onChange?: (html: string) => void;
|
|
4
4
|
className?: string;
|
|
5
|
+
/** 툴바 이미지 업로드와 이미지 붙여넣기에서 호출된다. 반환한 URL이 img의 src가 된다. */
|
|
5
6
|
handleImageUpload?: (fileData: {
|
|
6
7
|
filebase64: string;
|
|
7
8
|
fileName: string;
|
|
8
9
|
}) => Promise<string | null>;
|
|
10
|
+
/**
|
|
11
|
+
* 본문에 그대로 둘 이미지 호스트(예: `['assets.presslearn.co.kr']`). 지정하면 붙여넣은 HTML에서
|
|
12
|
+
* 이 호스트가 아닌 이미지를 받아 handleImageUpload로 올리고, 받지 못하거나 업로드에 실패하면 뺀다.
|
|
13
|
+
* 지정하지 않으면 붙여넣은 외부 이미지는 그대로 둔다.
|
|
14
|
+
*/
|
|
15
|
+
allowedImageHosts?: string[];
|
|
9
16
|
}
|
|
10
|
-
declare const LDEditor: ({ content, onChange, className, handleImageUpload, }: LDEditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
declare const LDEditor: ({ content, onChange, className, handleImageUpload, allowedImageHosts, }: LDEditorProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
18
|
export default LDEditor;
|
|
12
19
|
//# sourceMappingURL=LDEditor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LDEditor.d.ts","sourceRoot":"","sources":["../../../src/components/LDEditor/LDEditor.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LDEditor.d.ts","sourceRoot":"","sources":["../../../src/components/LDEditor/LDEditor.tsx"],"names":[],"mappings":"AAiKA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE;QAC7B,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAyUD,QAAA,MAAM,QAAQ,GAAI,yEAMf,aAAa,4CAqBf,CAAC;AA6lBF,eAAe,QAAQ,CAAC"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { Fragment, Slice, } from '@tiptap/pm/model';
|
|
4
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
5
|
+
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
6
|
+
import { defaultBlockAt, Extension, useEditorState, } from '@tiptap/react';
|
|
4
7
|
import { BubbleMenu } from '@tiptap/react/menus';
|
|
5
8
|
import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, ChevronDown, Image as ImageIcon, Italic, Link2, List, ListOrdered, Redo, Strikethrough, Table as TableIcon, Underline, Undo, Unlink, } from 'lucide-react';
|
|
6
|
-
import { useEffect, useState } from 'react';
|
|
9
|
+
import { useEffect, useRef, useState } from 'react';
|
|
7
10
|
import EditorBase, { useEditorContext } from '../../components/LDEditor/EditorBase.js';
|
|
8
11
|
import { createEditorExtensions } from '../../components/LDEditor/extensions.js';
|
|
9
12
|
import LDToast from '../../components/LDToast/LDToast.js';
|
|
@@ -69,11 +72,262 @@ const TableGridPicker = ({ onSelect, }) => {
|
|
|
69
72
|
}, className: cn('size-5 cursor-pointer rounded-xs border border-neutral200', filled && 'border-blue-500 bg-blue-100') }, index));
|
|
70
73
|
}) }), _jsx("p", { className: "mt-2 text-center text-sm", children: size.rows ? `${size.rows} x ${size.cols}` : '크기 선택' })] }));
|
|
71
74
|
};
|
|
72
|
-
const iconBtn = 'p-
|
|
75
|
+
const iconBtn = 'p-0.5 rounded-lg cursor-pointer transition hover:bg-mono100 disabled:opacity-30 disabled:cursor-not-allowed';
|
|
73
76
|
const activeBtn = 'bg-mono100';
|
|
74
77
|
const LDDivider = () => _jsx("div", { className: "mx-1 h-6 w-px bg-neutral200" });
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
// 구분선으로 나뉜 버튼 묶음. 툴바가 flex-wrap이라 묶음 자체가 하나의 flex 아이템이
|
|
79
|
+
// 되어, 폭이 모자라면 묶음 단위로 함께 다음 줄로 내려간다. 구분선은 묶음의 끝에
|
|
80
|
+
// 두어 줄이 바뀌어도 새 줄이 구분선으로 시작하지 않게 한다.
|
|
81
|
+
const ToolbarGroup = ({ children }) => (_jsx("div", { className: "flex items-center gap-2", children: children }));
|
|
82
|
+
// 검증을 통과하면 업로드한 URL을 반환하고, 실패하면 토스트에 띄울 사유를 담아 throw한다.
|
|
83
|
+
const uploadImageFile = async (file, handleImageUpload) => {
|
|
84
|
+
if (!validAllowedExtensions(file, TOOLBAR_ALLOWED_EXTENSIONS)) {
|
|
85
|
+
const allowedFiles = TOOLBAR_ALLOWED_EXTENSIONS.map((item) => FILE_EXTENSIONS[item].toUpperCase()).join(', ');
|
|
86
|
+
throw new Error(`지원하지 않는 파일 형식입니다. ${allowedFiles} 파일만 업로드 가능합니다.`);
|
|
87
|
+
}
|
|
88
|
+
if (!validFileSize(file, TOOLBAR_MAX_SIZE)) {
|
|
89
|
+
throw new Error(`파일 크기가 너무 큽니다. ${getFileSizeUnit(TOOLBAR_MAX_SIZE)} 이하 파일을 선택해주세요.`);
|
|
90
|
+
}
|
|
91
|
+
if (isImage(file.name) && !(await validImageSize(file))) {
|
|
92
|
+
throw new Error('이미지 해상도가 너무 큽니다. 더 작은 이미지를 선택해주세요.');
|
|
93
|
+
}
|
|
94
|
+
let src;
|
|
95
|
+
try {
|
|
96
|
+
const { fileBase64, fileName } = await fileToBase64(file);
|
|
97
|
+
src = await handleImageUpload?.({ filebase64: fileBase64, fileName });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new Error('파일 업로드 중 문제가 발생했습니다. 잠시 후 다시 시도해주세요.');
|
|
101
|
+
}
|
|
102
|
+
if (!src)
|
|
103
|
+
throw new Error('이미지 업로드에 실패했습니다.');
|
|
104
|
+
return src;
|
|
105
|
+
};
|
|
106
|
+
// 이미지를 넣을 본문 폭(px). CSS로 100%를 맞추려면 "크기를 정하지 않은 이미지"를
|
|
107
|
+
// 선택자로 가려내야 하는데, 그 조건이 빗나가면 래퍼와 이미지 폭이 어긋나 리사이즈
|
|
108
|
+
// 핸들이 이미지에서 떨어진다. 값으로 넣으면 구조와 무관하게 결과가 같다.
|
|
109
|
+
// 폭을 못 재면(숨겨진 컨테이너 등) undefined를 돌려 원본 크기로 둔다.
|
|
110
|
+
const getContentWidth = (dom) => {
|
|
111
|
+
const { paddingLeft, paddingRight } = getComputedStyle(dom);
|
|
112
|
+
const width = Math.round(dom.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight));
|
|
113
|
+
return width > 0 ? width : undefined;
|
|
114
|
+
};
|
|
115
|
+
// ── 이미지 붙여넣기 ──
|
|
116
|
+
const isDataUrl = (src) => typeof src === 'string' && src.startsWith('data:');
|
|
117
|
+
// 상대 경로는 현재 페이지 기준으로 푼다. 주소로 읽을 수 없으면 허용하지 않는다.
|
|
118
|
+
const isAllowedHost = (src, hosts) => {
|
|
119
|
+
try {
|
|
120
|
+
return hosts.includes(new URL(src, window.location.href).hostname);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
// 다른 호스트의 이미지를 받아 File로 만든다. 화면에 보여주는 것과 달리 내용을 읽는 건
|
|
127
|
+
// CORS를 허용하는 서버만 되므로, 대부분의 외부 사이트에서는 여기서 실패한다.
|
|
128
|
+
const urlToFile = async (url) => {
|
|
129
|
+
const response = await fetch(url).catch(() => null);
|
|
130
|
+
if (!response?.ok) {
|
|
131
|
+
throw new Error('다른 사이트의 이미지를 가져오지 못해 제외했습니다. 이미지를 저장한 뒤 직접 올려주세요.');
|
|
132
|
+
}
|
|
133
|
+
const blob = await response.blob();
|
|
134
|
+
return new File([blob], `image.${blob.type.slice('image/'.length)}`, {
|
|
135
|
+
type: blob.type,
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
// 툴바와 같은 검증을 태우려고 data URL을 File로 되돌린다.
|
|
139
|
+
const dataUrlToFile = async (dataUrl) => {
|
|
140
|
+
const [meta = '', base64 = ''] = dataUrl.split(',');
|
|
141
|
+
const type = /^data:(image\/[\w.+-]+);base64$/.exec(meta)?.[1];
|
|
142
|
+
if (!type)
|
|
143
|
+
throw new Error('이미지를 읽을 수 없습니다.');
|
|
144
|
+
return new File([Uint8Array.from(atob(base64), (char) => char.charCodeAt(0))], `image.${type.slice('image/'.length)}`, { type });
|
|
145
|
+
};
|
|
146
|
+
// 여러 장을 한 번에 올린다. 실패한 자리는 null이고, 사유는 첫 번째 것만 토스트로 알린다.
|
|
147
|
+
const uploadAll = async (uploads) => {
|
|
148
|
+
const results = await Promise.allSettled(uploads);
|
|
149
|
+
const failed = results.find((result) => result.status === 'rejected');
|
|
150
|
+
if (failed)
|
|
151
|
+
LDToast.error(failed.reason.message);
|
|
152
|
+
return results.map((result) => result.status === 'fulfilled' ? result.value : null);
|
|
153
|
+
};
|
|
154
|
+
// urls에 있는 이미지의 src를 업로드 URL로 바꾸고, 값이 null(업로드 실패)인 이미지는 뺀다.
|
|
155
|
+
const replaceImages = (fragment, urls) => {
|
|
156
|
+
const nodes = [];
|
|
157
|
+
fragment.forEach((node) => {
|
|
158
|
+
const src = node.attrs.src;
|
|
159
|
+
if (node.type.name === 'image' &&
|
|
160
|
+
typeof src === 'string' &&
|
|
161
|
+
urls.has(src)) {
|
|
162
|
+
const url = urls.get(src);
|
|
163
|
+
if (url)
|
|
164
|
+
nodes.push(node.type.create({ ...node.attrs, src: url }));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
nodes.push(node.childCount ? node.copy(replaceImages(node.content, urls)) : node);
|
|
168
|
+
});
|
|
169
|
+
return Fragment.from(nodes);
|
|
170
|
+
};
|
|
171
|
+
// 업로드 스켈레톤은 문서가 아닌 decoration이라 getHTML·onChange·실행취소 기록에 남지 않는다.
|
|
172
|
+
// meta에 pos가 있으면 추가, 없으면 id로 제거한다.
|
|
173
|
+
const placeholderKey = new PluginKey('imagePastePlaceholder');
|
|
174
|
+
const createSkeleton = () => {
|
|
175
|
+
const skeleton = document.createElement('span');
|
|
176
|
+
skeleton.setAttribute('role', 'status');
|
|
177
|
+
skeleton.setAttribute('aria-busy', 'true');
|
|
178
|
+
skeleton.setAttribute('aria-label', '이미지 업로드 중');
|
|
179
|
+
skeleton.className =
|
|
180
|
+
'block h-40 w-60 max-w-full animate-pulse rounded-md bg-mono200';
|
|
181
|
+
return skeleton;
|
|
182
|
+
};
|
|
183
|
+
const findPlaceholder = (state, id) => placeholderKey
|
|
184
|
+
.getState(state)
|
|
185
|
+
?.find(undefined, undefined, (spec) => spec.id === id)[0]
|
|
186
|
+
?.from;
|
|
187
|
+
// 업로드하는 동안 붙여넣은 자리에 스켈레톤을 띄우고, 끝나면 그 자리에 slice를 넣는다.
|
|
188
|
+
// 스켈레톤은 이미지 블록이 들어갈 자리라 블록 사이에 두고, 커서는 그 아래 블록으로 보내
|
|
189
|
+
// 업로드 중에도 아래에 이어 쓸 수 있게 한다. 편집으로 자리가 옮겨져도 따라가며,
|
|
190
|
+
// 스켈레톤 자리가 지워지면(아래 줄 맨 앞에서 Backspace 등) 결과를 버린다.
|
|
191
|
+
const pasteAfterUpload = (view, upload) => {
|
|
192
|
+
const id = {};
|
|
193
|
+
const tr = view.state.tr.deleteSelection();
|
|
194
|
+
const { $from } = tr.selection;
|
|
195
|
+
// 줄 중간·끝이면 Enter처럼 블록을 나눈다. 끝에서 나눌 땐 제목이 이어지지 않게 기본 블록(문단)으로 만든다.
|
|
196
|
+
if ($from.parent.isTextblock && $from.parentOffset > 0) {
|
|
197
|
+
const atEnd = $from.parentOffset === $from.parent.content.size;
|
|
198
|
+
const type = atEnd
|
|
199
|
+
? defaultBlockAt($from.node(-1).contentMatchAt($from.indexAfter(-1)))
|
|
200
|
+
: null;
|
|
201
|
+
tr.split($from.pos, 1, type ? [{ type }] : undefined);
|
|
202
|
+
}
|
|
203
|
+
const $cursor = tr.selection.$from;
|
|
204
|
+
const pos = $cursor.depth ? $cursor.before() : $cursor.pos;
|
|
205
|
+
view.dispatch(tr.setMeta(placeholderKey, { id, pos }).scrollIntoView());
|
|
206
|
+
void upload.then((slice) => {
|
|
207
|
+
if (view.isDestroyed)
|
|
208
|
+
return;
|
|
209
|
+
const pos = findPlaceholder(view.state, id);
|
|
210
|
+
if (pos === undefined)
|
|
211
|
+
return;
|
|
212
|
+
const tr = view.state.tr.setMeta(placeholderKey, { id });
|
|
213
|
+
if (slice.content.size) {
|
|
214
|
+
tr.replaceRange(pos, pos, slice)
|
|
215
|
+
.scrollIntoView()
|
|
216
|
+
.setMeta('paste', true)
|
|
217
|
+
.setMeta('uiEvent', 'paste');
|
|
218
|
+
}
|
|
219
|
+
view.dispatch(tr);
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* 붙여넣은 이미지 바이너리를 handleImageUpload가 반환한 URL로 바꿔 넣는다.
|
|
224
|
+
* - HTML 속 `data:` 이미지: 업로드 URL로 src를 교체한다.
|
|
225
|
+
* - allowedImageHosts를 지정했다면 HTML 속 그 밖의 호스트 이미지: 받아서 업로드하고 src를
|
|
226
|
+
* 교체한다. 받지 못하거나 업로드에 실패하면 뺀다.
|
|
227
|
+
* - HTML 없이 이미지 파일만 온 경우(스크린샷 등): 업로드 URL로 이미지를 넣는다.
|
|
228
|
+
* 업로드가 끝난 뒤 삽입해 base64가 onChange로 나가지 않게 하고, 그동안은 스켈레톤을 띄운다.
|
|
229
|
+
*/
|
|
230
|
+
const ImagePaste = Extension.create({
|
|
231
|
+
name: 'imagePaste',
|
|
232
|
+
addOptions() {
|
|
233
|
+
return { getConfig: () => ({}) };
|
|
234
|
+
},
|
|
235
|
+
addProseMirrorPlugins() {
|
|
236
|
+
const { getConfig } = this.options;
|
|
237
|
+
return [
|
|
238
|
+
new Plugin({
|
|
239
|
+
key: placeholderKey,
|
|
240
|
+
state: {
|
|
241
|
+
init: () => DecorationSet.empty,
|
|
242
|
+
apply: (tr, set) => {
|
|
243
|
+
const mapped = set.map(tr.mapping, tr.doc);
|
|
244
|
+
const action = tr.getMeta(placeholderKey);
|
|
245
|
+
if (!action)
|
|
246
|
+
return mapped;
|
|
247
|
+
if (action.pos !== undefined) {
|
|
248
|
+
return mapped.add(tr.doc, [
|
|
249
|
+
Decoration.widget(action.pos, createSkeleton, {
|
|
250
|
+
id: action.id,
|
|
251
|
+
}),
|
|
252
|
+
]);
|
|
253
|
+
}
|
|
254
|
+
return mapped.remove(mapped.find(undefined, undefined, (spec) => spec.id === action.id));
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
props: {
|
|
258
|
+
decorations: (state) => placeholderKey.getState(state),
|
|
259
|
+
handlePaste: (view, event, slice) => {
|
|
260
|
+
const { handleImageUpload, allowedImageHosts } = getConfig();
|
|
261
|
+
if (!handleImageUpload)
|
|
262
|
+
return false;
|
|
263
|
+
const html = event.clipboardData?.getData('text/html') ?? '';
|
|
264
|
+
const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith('image/'));
|
|
265
|
+
// data: 이미지는 항상, 허용 호스트를 지정했다면 그 밖의 호스트 이미지도 업로드 대상이다.
|
|
266
|
+
// 에디터에서 복사한 이미지는 이미 허용 호스트에 있으므로 다시 올리지 않는다.
|
|
267
|
+
const sources = new Set();
|
|
268
|
+
slice.content.descendants((node) => {
|
|
269
|
+
const src = node.attrs.src;
|
|
270
|
+
if (node.type.name !== 'image' || typeof src !== 'string')
|
|
271
|
+
return;
|
|
272
|
+
if (isDataUrl(src) ||
|
|
273
|
+
(allowedImageHosts && !isAllowedHost(src, allowedImageHosts))) {
|
|
274
|
+
sources.add(src);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
if (sources.size) {
|
|
278
|
+
const list = [...sources];
|
|
279
|
+
// 웹에서 "이미지 복사"하면 img 하나뿐인 HTML과 그 이미지 파일이 함께 온다.
|
|
280
|
+
// CORS와 무관하게 읽을 수 있는 파일 쪽을 쓴다.
|
|
281
|
+
const copiedImage = slice.content.childCount === 1 &&
|
|
282
|
+
slice.content.firstChild?.type.name === 'image' &&
|
|
283
|
+
files.length === 1
|
|
284
|
+
? files[0]
|
|
285
|
+
: undefined;
|
|
286
|
+
pasteAfterUpload(view, uploadAll(list.map((src) => isDataUrl(src)
|
|
287
|
+
? dataUrlToFile(src).then((file) => uploadImageFile(file, handleImageUpload))
|
|
288
|
+
: (copiedImage
|
|
289
|
+
? Promise.resolve(copiedImage)
|
|
290
|
+
: urlToFile(src)).then((file) => uploadImageFile(file, handleImageUpload)))).then((srcs) => {
|
|
291
|
+
const urls = new Map(list.map((src, index) => [src, srcs[index] ?? null]));
|
|
292
|
+
return new Slice(replaceImages(slice.content, urls), slice.openStart, slice.openEnd);
|
|
293
|
+
}));
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
// Office는 HTML과 함께 선택 영역을 찍은 PNG를 files에 넣는다. HTML이 있으면 파일은 무시한다.
|
|
297
|
+
if (html || !files.length)
|
|
298
|
+
return false;
|
|
299
|
+
pasteAfterUpload(view, uploadAll(files.map((file) => uploadImageFile(file, handleImageUpload))).then((srcs) => {
|
|
300
|
+
const nodes = files.flatMap((file, index) => {
|
|
301
|
+
const src = srcs[index];
|
|
302
|
+
return src
|
|
303
|
+
? [
|
|
304
|
+
view.state.schema.node('image', {
|
|
305
|
+
src,
|
|
306
|
+
alt: file.name,
|
|
307
|
+
width: getContentWidth(view.dom),
|
|
308
|
+
}),
|
|
309
|
+
]
|
|
310
|
+
: [];
|
|
311
|
+
});
|
|
312
|
+
return new Slice(Fragment.from(nodes), 0, 0);
|
|
313
|
+
}));
|
|
314
|
+
return true;
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
318
|
+
];
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const LDEditor = ({ content, onChange, className, handleImageUpload, allowedImageHosts, }) => {
|
|
322
|
+
// 에디터는 첫 렌더의 extensions로 한 번만 만들어진다. 붙여넣기는 최신 props를 ref로 읽는다.
|
|
323
|
+
const pasteConfigRef = useRef({ handleImageUpload, allowedImageHosts });
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
pasteConfigRef.current = { handleImageUpload, allowedImageHosts };
|
|
326
|
+
});
|
|
327
|
+
return (_jsxs(EditorBase.Root, { extensions: [
|
|
328
|
+
...createEditorExtensions(),
|
|
329
|
+
ImagePaste.configure({ getConfig: () => pasteConfigRef.current }),
|
|
330
|
+
], content: content, onChange: onChange, className: className, children: [_jsx(EditorToolbar, { handleImageUpload: handleImageUpload }), _jsx(EditorBase.Content, {})] }));
|
|
77
331
|
};
|
|
78
332
|
const EditorToolbar = ({ handleImageUpload, }) => {
|
|
79
333
|
const [linkUrl, setLinkUrl] = useState('');
|
|
@@ -120,111 +374,97 @@ const EditorToolbar = ({ handleImageUpload, }) => {
|
|
|
120
374
|
if (!files?.[0] || !editor)
|
|
121
375
|
return;
|
|
122
376
|
const file = files[0];
|
|
123
|
-
if (!validAllowedExtensions(file, TOOLBAR_ALLOWED_EXTENSIONS)) {
|
|
124
|
-
const allowedFiles = TOOLBAR_ALLOWED_EXTENSIONS.map((item) => FILE_EXTENSIONS[item].toUpperCase()).join(', ');
|
|
125
|
-
LDToast.error(`지원하지 않는 파일 형식입니다. ${allowedFiles} 파일만 업로드 가능합니다.`);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
if (!validFileSize(file, TOOLBAR_MAX_SIZE)) {
|
|
129
|
-
LDToast.error(`파일 크기가 너무 큽니다. ${getFileSizeUnit(TOOLBAR_MAX_SIZE)} 이하 파일을 선택해주세요.`);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
if (isImage(file.name) && !(await validImageSize(file))) {
|
|
133
|
-
LDToast.error('이미지 해상도가 너무 큽니다. 더 작은 이미지를 선택해주세요.');
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
377
|
try {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
378
|
+
const src = await uploadImageFile(file, handleImageUpload);
|
|
379
|
+
editor.commands.setImage({
|
|
380
|
+
src,
|
|
381
|
+
alt: file.name,
|
|
382
|
+
width: getContentWidth(editor.view.dom),
|
|
141
383
|
});
|
|
142
|
-
if (!res) {
|
|
143
|
-
LDToast.error('이미지 업로드에 실패했습니다.');
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
editor.commands.setImage({ src: res, alt: fileName });
|
|
147
384
|
}
|
|
148
|
-
catch {
|
|
149
|
-
LDToast.error(
|
|
385
|
+
catch (error) {
|
|
386
|
+
LDToast.error(error.message);
|
|
150
387
|
}
|
|
151
388
|
};
|
|
152
389
|
if (!editor || !state)
|
|
153
390
|
return null;
|
|
154
|
-
return (_jsxs(_Fragment, { children: [_jsxs(EditorBase.Toolbar, { className: "gap-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
e.preventDefault();
|
|
160
|
-
if (level === 0)
|
|
161
|
-
editor.chain().focus().setParagraph().run();
|
|
162
|
-
else
|
|
163
|
-
editor.chain().focus().toggleHeading({ level }).run();
|
|
164
|
-
close();
|
|
165
|
-
}, className: "w-full cursor-pointer px-3 py-2 text-left text-sm first:rounded-t-lg last:rounded-b-lg hover:bg-mono100", children: label }, level))) }), !isMobile && (_jsxs(_Fragment, { children: [_jsx(LDDivider, {}), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleBold().run(), className: cn(iconBtn, state.isBold && activeBtn), "aria-label": "\uAD75\uAC8C", children: _jsx(Bold, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleItalic().run(), className: cn(iconBtn, state.isItalic && activeBtn), "aria-label": "\uAE30\uC6B8\uC784", children: _jsx(Italic, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleUnderline().run(), className: cn(iconBtn, state.isUnderline && activeBtn), "aria-label": "\uBC11\uC904", children: _jsx(Underline, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleStrike().run(), className: cn(iconBtn, state.isStrike && activeBtn), "aria-label": "\uCDE8\uC18C\uC120", children: _jsx(Strikethrough, { className: "size-5" }) }), _jsx(LDDivider, {}), _jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-25 rounded-lg border border-neutral200 bg-white shadow-lg", trigger: ({ toggle }) => (_jsxs(EditorBase.Toolbar.LDButton, { "aria-label": "\uAE00\uC790 \uD06C\uAE30", onClick: toggle, className: cn(iconBtn, 'flex items-center gap-1 px-2'), children: [_jsx("span", { className: "text-sm whitespace-nowrap", children: state.fontSize }), _jsx(ChevronDown, { className: "size-4" })] })), children: ({ close }) => FONT_SIZES.map((size) => (_jsx("button", { type: "button", onClick: (e) => {
|
|
166
|
-
e.preventDefault();
|
|
167
|
-
editor.chain().focus().setFontSize(size).run();
|
|
168
|
-
close();
|
|
169
|
-
}, className: "w-full cursor-pointer px-3 py-2 text-left text-sm first:rounded-t-lg last:rounded-b-lg hover:bg-mono100", children: size }, size))) }), _jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-35 rounded-lg border border-neutral200 bg-white shadow-lg", trigger: ({ toggle }) => (_jsxs(EditorBase.Toolbar.LDButton, { "aria-label": "\uAE00\uC790 \uC0C9", onClick: toggle, className: cn(iconBtn, 'flex items-center gap-2 px-2'), children: [_jsx("span", { className: "size-4 rounded-full", style: { backgroundColor: state.color } }), state.color, _jsx(ChevronDown, { className: "size-4" })] })), children: ({ close }) => COLORS.map((color) => (_jsxs("button", { type: "button", onClick: (e) => {
|
|
391
|
+
return (_jsxs(_Fragment, { children: [_jsxs(EditorBase.Toolbar, { className: "gap-2 rounded-t-md border border-neutral200 px-4 py-3", children: [_jsxs(ToolbarGroup, { children: [_jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-30 rounded-lg border border-neutral200 bg-white shadow-lg", trigger: ({ toggle }) => (
|
|
392
|
+
// aria-label이 없으면 접근성 이름이 현재 선택값('본문'·'제목 1'…)이 되어,
|
|
393
|
+
// 드롭다운이 열렸을 때 트리거와 옵션이 같은 이름을 갖는다.
|
|
394
|
+
// 고정 라벨을 줘서 트리거를 항상 같은 이름으로 가리킬 수 있게 한다.
|
|
395
|
+
_jsx(EditorBase.Toolbar.LDButton, { "aria-label": "\uC81C\uBAA9 \uC218\uC900", onClick: toggle, className: cn(iconBtn, 'flex items-center gap-5 px-2'), children: _jsx("span", { className: "min-w-10 whitespace-nowrap", children: state.headingLevel ? `제목 ${state.headingLevel}` : '본문' }) })), children: ({ close }) => HEADINGS.map(({ label, level }) => (_jsx("button", { type: "button", onClick: (e) => {
|
|
170
396
|
e.preventDefault();
|
|
171
|
-
|
|
397
|
+
if (level === 0)
|
|
398
|
+
editor.chain().focus().setParagraph().run();
|
|
399
|
+
else
|
|
400
|
+
editor.chain().focus().toggleHeading({ level }).run();
|
|
172
401
|
close();
|
|
173
|
-
}, className: "
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
chain
|
|
201
|
-
|
|
202
|
-
|
|
402
|
+
}, className: "w-full cursor-pointer px-3 py-2 text-left text-sm first:rounded-t-lg last:rounded-b-lg hover:bg-mono100", children: label }, level))) }), _jsx(LDDivider, {})] }), !isMobile && (_jsxs(_Fragment, { children: [_jsxs(ToolbarGroup, { children: [_jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleBold().run(), className: cn(iconBtn, state.isBold && activeBtn), "aria-label": "\uAD75\uAC8C", children: _jsx(Bold, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleItalic().run(), className: cn(iconBtn, state.isItalic && activeBtn), "aria-label": "\uAE30\uC6B8\uC784", children: _jsx(Italic, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleUnderline().run(), className: cn(iconBtn, state.isUnderline && activeBtn), "aria-label": "\uBC11\uC904", children: _jsx(Underline, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleStrike().run(), className: cn(iconBtn, state.isStrike && activeBtn), "aria-label": "\uCDE8\uC18C\uC120", children: _jsx(Strikethrough, { className: "size-5" }) }), _jsx(LDDivider, {})] }), _jsxs(ToolbarGroup, { children: [_jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-25 rounded-lg border border-neutral200 bg-white shadow-lg", trigger: ({ toggle }) => (_jsxs(EditorBase.Toolbar.LDButton, { "aria-label": "\uAE00\uC790 \uD06C\uAE30", onClick: toggle, className: cn(iconBtn, 'flex items-center gap-1 px-2'), children: [_jsx("span", { className: "text-sm whitespace-nowrap", children: state.fontSize }), _jsx(ChevronDown, { className: "size-4" })] })), children: ({ close }) => FONT_SIZES.map((size) => (_jsx("button", { type: "button", onClick: (e) => {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
editor.chain().focus().setFontSize(size).run();
|
|
405
|
+
close();
|
|
406
|
+
}, className: "w-full cursor-pointer px-3 py-2 text-left text-sm first:rounded-t-lg last:rounded-b-lg hover:bg-mono100", children: size }, size))) }), _jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-35 rounded-lg border border-neutral200 bg-white shadow-lg", trigger: ({ toggle }) => (_jsxs(EditorBase.Toolbar.LDButton, { "aria-label": "\uAE00\uC790 \uC0C9", onClick: toggle, className: cn(iconBtn, 'flex items-center gap-2 px-2'), children: [_jsx("span", { className: "size-4 rounded-full", style: { backgroundColor: state.color } }), state.color, _jsx(ChevronDown, { className: "size-4" })] })), children: ({ close }) => COLORS.map((color) => (_jsxs("button", { type: "button", onClick: (e) => {
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
editor.chain().focus().setColor(color.value).run();
|
|
409
|
+
close();
|
|
410
|
+
}, className: "flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm first:rounded-t-lg last:rounded-b-lg hover:bg-mono100", children: [_jsx("span", { className: "size-4 rounded-full", style: { backgroundColor: color.value } }), _jsx("span", { children: color.label })] }, color.value))) }), _jsx(LDDivider, {})] }), _jsxs(ToolbarGroup, { children: [['left', 'center', 'right', 'justify'].map((align) => {
|
|
411
|
+
const Icon = {
|
|
412
|
+
left: AlignLeft,
|
|
413
|
+
center: AlignCenter,
|
|
414
|
+
right: AlignRight,
|
|
415
|
+
justify: AlignJustify,
|
|
416
|
+
}[align];
|
|
417
|
+
const alignLabel = {
|
|
418
|
+
left: '왼쪽 정렬',
|
|
419
|
+
center: '가운데 정렬',
|
|
420
|
+
right: '오른쪽 정렬',
|
|
421
|
+
justify: '양쪽 정렬',
|
|
422
|
+
}[align];
|
|
423
|
+
return (_jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().setTextAlign(align).run(), className: cn(iconBtn, state.align[align] && activeBtn), "aria-label": alignLabel, children: _jsx(Icon, { className: "size-5" }) }, align));
|
|
424
|
+
}), _jsx(LDDivider, {})] })] })), _jsxs(ToolbarGroup, { children: [_jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleBulletList().run(), className: cn(iconBtn, state.isBulletList && activeBtn), "aria-label": "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", children: _jsx(List, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleOrderedList().run(), className: cn(iconBtn, state.isOrderedList && activeBtn), "aria-label": "\uBC88\uD638 \uBAA9\uB85D", children: _jsx(ListOrdered, { className: "size-5" }) }), _jsx(LDDivider, {})] }), _jsxs(ToolbarGroup, { children: [_jsx(EditorBase.Toolbar.LDButton, { disabled: !state.canUndo, onClick: () => editor.chain().focus().undo().run(), className: iconBtn, "aria-label": "\uB418\uB3CC\uB9AC\uAE30", children: _jsx(Undo, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { disabled: !state.canRedo, onClick: () => editor.chain().focus().redo().run(), className: iconBtn, "aria-label": "\uB2E4\uC2DC \uC2E4\uD589", children: _jsx(Redo, { className: "size-5" }) }), _jsx(LDDivider, {})] }), !isMobile && (_jsx(_Fragment, { children: _jsxs(ToolbarGroup, { children: [_jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-70 rounded-lg border border-neutral200 bg-white p-3 shadow-lg", trigger: ({ toggle }) => (_jsx(EditorBase.Toolbar.LDButton, { disabled: !state.hasSelection, onClick: () => {
|
|
425
|
+
setLinkUrl(editor.getAttributes('link').href || '');
|
|
426
|
+
toggle();
|
|
427
|
+
}, className: cn(iconBtn, state.isLink && activeBtn), "aria-label": "\uB9C1\uD06C", children: _jsx(Link2, { className: "size-5" }) })), children: ({ close }) => {
|
|
428
|
+
const applyLink = () => {
|
|
429
|
+
const chain = editor
|
|
430
|
+
.chain()
|
|
431
|
+
.focus()
|
|
432
|
+
.extendMarkRange('link');
|
|
433
|
+
if (linkUrl === '') {
|
|
434
|
+
chain.unsetLink().run();
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
const url = /^https?:\/\//.test(linkUrl)
|
|
438
|
+
? linkUrl
|
|
439
|
+
: `https://${linkUrl}`;
|
|
440
|
+
chain.setLink({ href: url }).run();
|
|
441
|
+
}
|
|
442
|
+
setLinkUrl('');
|
|
443
|
+
close();
|
|
444
|
+
};
|
|
445
|
+
return (_jsxs(_Fragment, { children: [_jsx("input", { type: "text", autoFocus: true, placeholder: "URL\uC744 \uC785\uB825\uD558\uC138\uC694", value: linkUrl, onChange: (e) => setLinkUrl(e.target.value), onKeyDown: (e) => e.key === 'Enter' && applyLink(), className: "w-full rounded-lg border border-neutral200 px-3 py-2 text-sm transition duration-300 outline-none focus:border-blue-500" }), _jsxs("div", { className: "mt-2 flex gap-2", children: [_jsx("button", { type: "button", onClick: (e) => {
|
|
446
|
+
e.preventDefault();
|
|
447
|
+
applyLink();
|
|
448
|
+
}, className: "flex-1 rounded-lg bg-blue-500 px-3 py-1.5 text-sm text-white transition duration-300 hover:bg-blue-600", children: "\uC801\uC6A9" }), _jsx("button", { type: "button", onClick: (e) => {
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
setLinkUrl('');
|
|
451
|
+
close();
|
|
452
|
+
}, className: "rounded-lg border border-neutral200 px-3 py-1.5 text-sm transition duration-300 hover:bg-mono100", children: "\uCDE8\uC18C" })] })] }));
|
|
453
|
+
} }), _jsx(EditorBase.Toolbar.LDButton, { disabled: !state.isLink, onClick: () => editor.chain().focus().unsetLink().run(), className: iconBtn, "aria-label": "\uB9C1\uD06C \uD574\uC81C", children: _jsx(Unlink, { className: "size-5" }) }), _jsx(LDDivider, {})] }) })), _jsxs(ToolbarGroup, { children: [_jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-40 rounded-lg border border-neutral200 bg-white shadow-lg", trigger: ({ toggle }) => (_jsx(EditorBase.Toolbar.LDButton, { "aria-label": "\uD45C", onClick: toggle, className: cn(iconBtn, state.isTable && activeBtn), children: _jsx(TableIcon, { className: "size-5" }) })), children: ({ close }) => state.isTable ? (_jsx("div", { children: TABLE_ACTIONS.map(({ label, run }) => (_jsx("button", { type: "button", onClick: (e) => {
|
|
454
|
+
e.preventDefault();
|
|
455
|
+
run(editor);
|
|
456
|
+
close();
|
|
457
|
+
}, className: "w-full cursor-pointer px-3 py-2 text-left text-sm first:rounded-t-lg last:rounded-b-lg hover:bg-mono100", children: label }, label))) })) : (_jsx(TableGridPicker, { onSelect: (rows, cols) => {
|
|
458
|
+
editor
|
|
459
|
+
.chain()
|
|
460
|
+
.focus()
|
|
461
|
+
.insertTable({ rows, cols, withHeaderRow: true })
|
|
462
|
+
.run();
|
|
203
463
|
close();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}, className: "flex-1 rounded-lg bg-blue-500 px-3 py-1.5 text-sm text-white transition duration-300 hover:bg-blue-600", children: "\uC801\uC6A9" }), _jsx("button", { type: "button", onClick: (e) => {
|
|
209
|
-
e.preventDefault();
|
|
210
|
-
setLinkUrl('');
|
|
211
|
-
close();
|
|
212
|
-
}, className: "rounded-lg border border-neutral200 px-3 py-1.5 text-sm transition duration-300 hover:bg-mono100", children: "\uCDE8\uC18C" })] })] }));
|
|
213
|
-
} }), _jsx(EditorBase.Toolbar.LDButton, { disabled: !state.isLink, onClick: () => editor.chain().focus().unsetLink().run(), className: iconBtn, "aria-label": "\uB9C1\uD06C \uD574\uC81C", children: _jsx(Unlink, { className: "size-5" }) })] })), _jsx(LDDivider, {}), _jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-40 rounded-lg border border-neutral200 bg-white shadow-lg", trigger: ({ toggle }) => (_jsx(EditorBase.Toolbar.LDButton, { "aria-label": "\uD45C", onClick: toggle, className: cn(iconBtn, state.isTable && activeBtn), children: _jsx(TableIcon, { className: "size-5" }) })), children: ({ close }) => state.isTable ? (_jsx("div", { children: TABLE_ACTIONS.map(({ label, run }) => (_jsx("button", { type: "button", onClick: (e) => {
|
|
214
|
-
e.preventDefault();
|
|
215
|
-
run(editor);
|
|
216
|
-
close();
|
|
217
|
-
}, className: "w-full cursor-pointer px-3 py-2 text-left text-sm first:rounded-t-lg last:rounded-b-lg hover:bg-mono100", children: label }, label))) })) : (_jsx(TableGridPicker, { onSelect: (rows, cols) => {
|
|
218
|
-
editor
|
|
219
|
-
.chain()
|
|
220
|
-
.focus()
|
|
221
|
-
.insertTable({ rows, cols, withHeaderRow: true })
|
|
222
|
-
.run();
|
|
223
|
-
close();
|
|
224
|
-
} })) }), _jsxs("label", { className: cn(iconBtn, 'relative'), children: [_jsx(ImageIcon, { className: "size-5" }), _jsx("input", { type: "file", accept: "image/*", "aria-label": "\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC", className: "absolute inset-0 size-full cursor-pointer opacity-0", onChange: (e) => {
|
|
225
|
-
handleImageUploadAction(e.target.files);
|
|
226
|
-
e.target.value = '';
|
|
227
|
-
} })] })] }), isMobile && (_jsxs(BubbleMenu, { editor: editor, className: "flex items-center gap-1 rounded-lg border border-neutral200 bg-white p-1 shadow-lg", children: [_jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleBold().run(), className: cn(iconBtn, state.isBold && activeBtn), "aria-label": "\uAD75\uAC8C", children: _jsx(Bold, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleItalic().run(), className: cn(iconBtn, state.isItalic && activeBtn), "aria-label": "\uAE30\uC6B8\uC784", children: _jsx(Italic, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleUnderline().run(), className: cn(iconBtn, state.isUnderline && activeBtn), "aria-label": "\uBC11\uC904", children: _jsx(Underline, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleStrike().run(), className: cn(iconBtn, state.isStrike && activeBtn), "aria-label": "\uCDE8\uC18C\uC120", children: _jsx(Strikethrough, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-35 rounded-lg border border-neutral200 bg-white shadow-lg", trigger: ({ toggle }) => (_jsxs(EditorBase.Toolbar.LDButton, { onClick: toggle, className: cn(iconBtn, 'flex items-center gap-1 px-2'), "aria-label": "\uAE00\uC790 \uC0C9", children: [_jsx("span", { className: "size-4 rounded-full", style: { backgroundColor: state.color } }), _jsx(ChevronDown, { className: "size-4" })] })), children: ({ close }) => COLORS.map((color) => (_jsxs("button", { type: "button", onClick: (e) => {
|
|
464
|
+
} })) }), _jsxs("label", { className: cn(iconBtn, 'relative'), children: [_jsx(ImageIcon, { className: "size-5" }), _jsx("input", { type: "file", accept: "image/*", "aria-label": "\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC", className: "absolute inset-0 size-full cursor-pointer opacity-0", onChange: (e) => {
|
|
465
|
+
handleImageUploadAction(e.target.files);
|
|
466
|
+
e.target.value = '';
|
|
467
|
+
} })] })] })] }), isMobile && (_jsxs(BubbleMenu, { editor: editor, className: "flex items-center gap-1 rounded-lg border border-neutral200 bg-white p-1 shadow-lg", children: [_jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleBold().run(), className: cn(iconBtn, state.isBold && activeBtn), "aria-label": "\uAD75\uAC8C", children: _jsx(Bold, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleItalic().run(), className: cn(iconBtn, state.isItalic && activeBtn), "aria-label": "\uAE30\uC6B8\uC784", children: _jsx(Italic, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleUnderline().run(), className: cn(iconBtn, state.isUnderline && activeBtn), "aria-label": "\uBC11\uC904", children: _jsx(Underline, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleStrike().run(), className: cn(iconBtn, state.isStrike && activeBtn), "aria-label": "\uCDE8\uC18C\uC120", children: _jsx(Strikethrough, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-35 rounded-lg border border-neutral200 bg-white shadow-lg", trigger: ({ toggle }) => (_jsxs(EditorBase.Toolbar.LDButton, { onClick: toggle, className: cn(iconBtn, 'flex items-center gap-1 px-2'), "aria-label": "\uAE00\uC790 \uC0C9", children: [_jsx("span", { className: "size-4 rounded-full", style: { backgroundColor: state.color } }), _jsx(ChevronDown, { className: "size-4" })] })), children: ({ close }) => COLORS.map((color) => (_jsxs("button", { type: "button", onClick: (e) => {
|
|
228
468
|
e.preventDefault();
|
|
229
469
|
editor.chain().focus().setColor(color.value).run();
|
|
230
470
|
close();
|
|
@@ -24,6 +24,8 @@ export interface LDModalOptions<T = undefined> {
|
|
|
24
24
|
scrollBehavior?: 'inside' | 'outside';
|
|
25
25
|
/** 오버레이(모달 바깥) 클릭으로 닫기 허용 여부 */
|
|
26
26
|
closeOnOverlayClick?: boolean;
|
|
27
|
+
/** ESC 키로 닫기 허용 여부 */
|
|
28
|
+
closeOnEsc?: boolean;
|
|
27
29
|
/** 모달 패널에 적용할 추가 CSS 클래스 */
|
|
28
30
|
className?: string;
|
|
29
31
|
/** 모달 패널 인라인 스타일 (자유 width/height 등) */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LDModal.d.ts","sourceRoot":"","sources":["../../../src/components/LDModal/LDModal.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAoB,MAAM,OAAO,CAAC;AAG7E,OAAoB,EAElB,KAAK,SAAS,EACd,gBAAgB,EACjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,SAAS;IAC3C,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,OAAO,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,IAAI,CAAA;KAAE,KAAK,SAAS,CAAC,CAAC;IAC9E,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,CAAC,CAAC;QACT,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC;QACzD,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;QACvC,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC;KAC1D,EAAE,CAAC;IACJ,eAAe;IACf,gBAAgB,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IACpC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,+BAA+B;IAC/B,cAAc,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IACtC,gCAAgC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACjD,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED,QAAA,MAAM,OAAO,GAAI,CAAC,GAAG,SAAS,EAC5B,SAAS,cAAc,CAAC,CAAC,CAAC,KACzB,OAAO,CAAC,CAAC,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"LDModal.d.ts","sourceRoot":"","sources":["../../../src/components/LDModal/LDModal.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAoB,MAAM,OAAO,CAAC;AAG7E,OAAoB,EAElB,KAAK,SAAS,EACd,gBAAgB,EACjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,SAAS;IAC3C,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,OAAO,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,IAAI,CAAA;KAAE,KAAK,SAAS,CAAC,CAAC;IAC9E,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,CAAC,CAAC;QACT,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC;QACzD,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;QACvC,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC;KAC1D,EAAE,CAAC;IACJ,eAAe;IACf,gBAAgB,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IACpC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,+BAA+B;IAC/B,cAAc,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IACtC,gCAAgC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,sBAAsB;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACjD,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED,QAAA,MAAM,OAAO,GAAI,CAAC,GAAG,SAAS,EAC5B,SAAS,cAAc,CAAC,CAAC,CAAC,KACzB,OAAO,CAAC,CAAC,GAAG,SAAS,CA6EvB,CAAC;AAEF,eAAe,OAAO,CAAC"}
|
|
@@ -50,7 +50,7 @@ const LDModal = (options) => {
|
|
|
50
50
|
return (_jsx(LDModalView, { open: open, onOpenChange: (next) => {
|
|
51
51
|
if (!next)
|
|
52
52
|
close(undefined);
|
|
53
|
-
}, name: options.name, title: options.title, description: options.description, size: options.size, icon: options.icon, scrollBehavior: options.scrollBehavior, closeOnOverlayClick: options.closeOnOverlayClick, style: options.style, buttons: list ? { direction: options.buttonsDirection, list } : undefined, classNames: options.classNames, children: content }));
|
|
53
|
+
}, name: options.name, title: options.title, description: options.description, size: options.size, icon: options.icon, scrollBehavior: options.scrollBehavior, closeOnOverlayClick: options.closeOnOverlayClick, closeOnEsc: options.closeOnEsc, style: options.style, buttons: list ? { direction: options.buttonsDirection, list } : undefined, classNames: options.classNames, children: content }));
|
|
54
54
|
};
|
|
55
55
|
root.render(_jsx(ModalHost, {}));
|
|
56
56
|
});
|
|
@@ -52,12 +52,14 @@ export interface LDModalViewProps {
|
|
|
52
52
|
scrollBehavior?: 'inside' | 'outside';
|
|
53
53
|
/** 오버레이(모달 바깥) 클릭으로 닫기 허용 여부 */
|
|
54
54
|
closeOnOverlayClick?: boolean;
|
|
55
|
+
/** ESC 키로 닫기 허용 여부 */
|
|
56
|
+
closeOnEsc?: boolean;
|
|
55
57
|
/** 인라인 스타일 (모달 패널에 적용) */
|
|
56
58
|
style?: React.CSSProperties;
|
|
57
59
|
/** 버튼 클릭 시 해당 버튼의 value를 전달하는 콜백 */
|
|
58
60
|
onButtonClick?: (value: unknown) => void;
|
|
59
61
|
classNames?: TModalClassNames;
|
|
60
62
|
}
|
|
61
|
-
declare const LDModalView: ({ open, defaultOpen, onOpenChange, name, title, description, children, size, icon, buttons, scrollBehavior, closeOnOverlayClick, classNames, style, onButtonClick, }: LDModalViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
63
|
+
declare const LDModalView: ({ open, defaultOpen, onOpenChange, name, title, description, children, size, icon, buttons, scrollBehavior, closeOnOverlayClick, closeOnEsc, classNames, style, onButtonClick, }: LDModalViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
62
64
|
export default LDModalView;
|
|
63
65
|
//# sourceMappingURL=LDModalView.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LDModalView.d.ts","sourceRoot":"","sources":["../../../src/components/LDModal/LDModalView.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAU,MAAM,OAAO,CAAC;AAK/C,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAI7C,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;AAE9E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC7B,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,aAAa;IACb,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,EAAE,CAAC,CAAC;IACT,gBAAgB;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC;IACzD,eAAe;IACf,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IACvC,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,gBAAgB;IAC/B,yBAAyB;IACzB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0BAA0B;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yBAAyB;IACzB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,2BAA2B;IAC3B,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,YAAY;IACZ,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,gBAAgB;IAChB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,aAAa;IACb,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,gBAAgB;IAChB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,2BAA2B;IAC3B,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,eAAe;IACf,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,aAAa;IACb,cAAc,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IACtC,gCAAgC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,0BAA0B;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,oCAAoC;IACpC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AA6CD,QAAA,MAAM,WAAW,GAAI,
|
|
1
|
+
{"version":3,"file":"LDModalView.d.ts","sourceRoot":"","sources":["../../../src/components/LDModal/LDModalView.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAU,MAAM,OAAO,CAAC;AAK/C,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAI7C,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;AAE9E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC7B,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,aAAa;IACb,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,EAAE,CAAC,CAAC;IACT,gBAAgB;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC;IACzD,eAAe;IACf,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IACvC,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,gBAAgB;IAC/B,yBAAyB;IACzB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0BAA0B;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yBAAyB;IACzB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,2BAA2B;IAC3B,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,YAAY;IACZ,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,gBAAgB;IAChB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,aAAa;IACb,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,gBAAgB;IAChB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,2BAA2B;IAC3B,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,eAAe;IACf,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,aAAa;IACb,cAAc,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IACtC,gCAAgC;IAChC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,sBAAsB;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,oCAAoC;IACpC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AA6CD,QAAA,MAAM,WAAW,GAAI,kLAiBlB,gBAAgB,4CAoLlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
|
|
@@ -42,7 +42,7 @@ const statusIconSizeClasses = {
|
|
|
42
42
|
large: 'size-12.5',
|
|
43
43
|
xlarge: 'size-12.5',
|
|
44
44
|
};
|
|
45
|
-
const LDModalView = ({ open, defaultOpen, onOpenChange, name, title, description, children, size = 'small', icon, buttons, scrollBehavior = 'inside', closeOnOverlayClick = true, classNames, style, onButtonClick, }) => {
|
|
45
|
+
const LDModalView = ({ open, defaultOpen, onOpenChange, name, title, description, children, size = 'small', icon, buttons, scrollBehavior = 'inside', closeOnOverlayClick = true, closeOnEsc = true, classNames, style, onButtonClick, }) => {
|
|
46
46
|
const overlayRef = useRef(false);
|
|
47
47
|
const hasHeader = name || title || description || icon;
|
|
48
48
|
const handleClick = async (button) => {
|
|
@@ -57,14 +57,14 @@ const LDModalView = ({ open, defaultOpen, onOpenChange, name, title, description
|
|
|
57
57
|
return (_jsxs(ModalBase.Root, { open: open, defaultOpen: defaultOpen, onOpenChange: onOpenChange, children: [_jsx(ModalBase.Overlay, { className: classNames?.overlay }), _jsx(ModalBase.Content, { className: cn({
|
|
58
58
|
['top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2']: scrollBehavior === 'inside',
|
|
59
59
|
['inset-0 flex items-start justify-center overflow-y-auto py-20']: scrollBehavior === 'outside',
|
|
60
|
-
}, classNames?.content), onPointerDownOutside: closeOnOverlayClick ? undefined : (e) => e.preventDefault(), onMouseDown: (e) => (overlayRef.current = e.target === e.currentTarget), onClick: (e) => {
|
|
60
|
+
}, classNames?.content), onPointerDownOutside: closeOnOverlayClick ? undefined : (e) => e.preventDefault(), onEscapeKeyDown: closeOnEsc ? undefined : (e) => e.preventDefault(), onMouseDown: (e) => (overlayRef.current = e.target === e.currentTarget), onClick: (e) => {
|
|
61
61
|
if (closeOnOverlayClick &&
|
|
62
62
|
scrollBehavior === 'outside' &&
|
|
63
63
|
e.target === e.currentTarget &&
|
|
64
64
|
overlayRef.current)
|
|
65
65
|
onOpenChange?.(false);
|
|
66
66
|
overlayRef.current = false;
|
|
67
|
-
}, children: _jsxs("div", { className: cn('relative rounded-2xl bg-white p-5', sizeClasses[size], 'mobile:max-w-[calc(100vw-40px)]', scrollBehavior === 'inside' && 'flex max-h-[90dvh] flex-col', classNames?.layout), style: style, onClick: (e) => e.stopPropagation(), children: [_jsx(ModalBase.Close, { asChild: true, className: cn('absolute top-
|
|
67
|
+
}, children: _jsxs("div", { className: cn('relative rounded-2xl bg-white p-5', sizeClasses[size], 'mobile:max-w-[calc(100vw-40px)]', scrollBehavior === 'inside' && 'flex max-h-[90dvh] flex-col', classNames?.layout), style: style, onClick: (e) => e.stopPropagation(), children: [_jsx(ModalBase.Close, { asChild: true, className: cn('absolute top-5 right-5 z-10 flex cursor-pointer text-neutral500 outline-hidden transition-colors after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2 hover:text-neutral700', classNames?.close), children: _jsx("button", { type: "button", "aria-label": "\uB2EB\uAE30", children: _jsx(X, { className: "size-4 tablet:size-5 pc:size-6" }) }) }), _jsxs("div", { className: cn('flex w-full shrink-0 flex-col', scrollBehavior === 'inside' && 'min-h-0 flex-1', layoutGapSizeClasses[size], !hasHeader && noHeaderPaddingTopSizeClasses[size], classNames?.panel), children: [_jsx(Show, { when: hasHeader, children: _jsxs("div", { className: "flex w-full shrink-0 flex-col items-center gap-5 py-5 text-center", children: [_jsx(Show, { when: icon === 'success', children: _jsx("div", { className: cn('flex items-center justify-center rounded-full bg-hyper-blue', statusIconSizeClasses[size]), children: _jsx(Check, { className: "text-white" }) }) }), _jsx(Show, { when: icon === 'fail', children: _jsx("div", { className: cn('flex items-center justify-center rounded-full bg-danger600', statusIconSizeClasses[size]), children: _jsx(CircleAlert, { className: "text-white" }) }) }), _jsx(Show, { when: name, children: _jsx("span", { className: "display16 bold whitespace-pre-wrap text-neutral500", children: name }) }), _jsx(Show, { when: title, children: _jsx(ModalBase.Title, { className: cn('text-center display28 bold whitespace-pre-wrap', classNames?.title), children: title }) }), _jsx(Show, { when: description, children: _jsx(ModalBase.Description, { className: cn('text-center display14 whitespace-pre-wrap text-neutral500', classNames?.description), children: description }) })] }) }), _jsx(Show, { when: children, children: _jsx("div", { className: cn(scrollBehavior === 'inside' &&
|
|
68
68
|
'min-h-10 flex-1 overflow-y-auto'), children: children }) }), _jsx(Show, { when: buttons?.list?.length, children: _jsx("div", { className: cn('flex mobile:flex-1 mobile:flex-col mobile:items-center', buttons?.direction === 'column' ? 'flex-col' : 'flex-row', buttonsGapSizeClasses[size]), children: buttons?.list?.map((button, index) => {
|
|
69
69
|
const isFirst = index === 0;
|
|
70
70
|
return (_jsx("div", { className: "flex w-full flex-1", children: _jsx(LDButton, { type: "button", onClick: () => handleClick(button), variant: button.variant ?? (isFirst ? 'solid' : 'outline'), color: button.color ?? (isFirst ? 'point' : 'neutral'), size: buttonSizeClasses[size], block: true, children: button.label }) }, String(button.value)));
|
package/dist/index.css
CHANGED
|
@@ -433,6 +433,9 @@
|
|
|
433
433
|
.absolute {
|
|
434
434
|
position: absolute;
|
|
435
435
|
}
|
|
436
|
+
.absolute\! {
|
|
437
|
+
position: absolute !important;
|
|
438
|
+
}
|
|
436
439
|
.fixed {
|
|
437
440
|
position: fixed;
|
|
438
441
|
}
|
|
@@ -481,12 +484,18 @@
|
|
|
481
484
|
.top-4 {
|
|
482
485
|
top: calc(var(--spacing) * 4);
|
|
483
486
|
}
|
|
487
|
+
.top-5 {
|
|
488
|
+
top: calc(var(--spacing) * 5);
|
|
489
|
+
}
|
|
484
490
|
.top-16 {
|
|
485
491
|
top: calc(var(--spacing) * 16);
|
|
486
492
|
}
|
|
487
493
|
.top-20 {
|
|
488
494
|
top: calc(var(--spacing) * 20);
|
|
489
495
|
}
|
|
496
|
+
.top-\[17px\] {
|
|
497
|
+
top: 17px;
|
|
498
|
+
}
|
|
490
499
|
.top-\[40\%\] {
|
|
491
500
|
top: 40%;
|
|
492
501
|
}
|
|
@@ -1230,9 +1239,6 @@
|
|
|
1230
1239
|
.mt-2 {
|
|
1231
1240
|
margin-top: calc(var(--spacing) * 2);
|
|
1232
1241
|
}
|
|
1233
|
-
.mt-3 {
|
|
1234
|
-
margin-top: calc(var(--spacing) * 3);
|
|
1235
|
-
}
|
|
1236
1242
|
.mt-md {
|
|
1237
1243
|
margin-top: var(--spacing_20);
|
|
1238
1244
|
}
|
|
@@ -1248,9 +1254,6 @@
|
|
|
1248
1254
|
.-mb-px {
|
|
1249
1255
|
margin-bottom: -1px;
|
|
1250
1256
|
}
|
|
1251
|
-
.mb-1 {
|
|
1252
|
-
margin-bottom: calc(var(--spacing) * 1);
|
|
1253
|
-
}
|
|
1254
1257
|
.mb-2 {
|
|
1255
1258
|
margin-bottom: calc(var(--spacing) * 2);
|
|
1256
1259
|
}
|
|
@@ -1490,6 +1493,9 @@
|
|
|
1490
1493
|
.h-1\.5 {
|
|
1491
1494
|
height: calc(var(--spacing) * 1.5);
|
|
1492
1495
|
}
|
|
1496
|
+
.h-1\.5\! {
|
|
1497
|
+
height: calc(var(--spacing) * 1.5) !important;
|
|
1498
|
+
}
|
|
1493
1499
|
.h-2 {
|
|
1494
1500
|
height: calc(var(--spacing) * 2);
|
|
1495
1501
|
}
|
|
@@ -1743,6 +1749,9 @@
|
|
|
1743
1749
|
.w-16 {
|
|
1744
1750
|
width: calc(var(--spacing) * 16);
|
|
1745
1751
|
}
|
|
1752
|
+
.w-19\! {
|
|
1753
|
+
width: calc(var(--spacing) * 19) !important;
|
|
1754
|
+
}
|
|
1746
1755
|
.w-20 {
|
|
1747
1756
|
width: calc(var(--spacing) * 20);
|
|
1748
1757
|
}
|
|
@@ -2217,6 +2226,9 @@
|
|
|
2217
2226
|
.rounded-\[10px\] {
|
|
2218
2227
|
border-radius: 10px;
|
|
2219
2228
|
}
|
|
2229
|
+
.rounded-\[10px\]\! {
|
|
2230
|
+
border-radius: 10px !important;
|
|
2231
|
+
}
|
|
2220
2232
|
.rounded-\[12px\] {
|
|
2221
2233
|
border-radius: 12px;
|
|
2222
2234
|
}
|
|
@@ -2247,9 +2259,9 @@
|
|
|
2247
2259
|
.rounded-xs {
|
|
2248
2260
|
border-radius: var(--radius-xs);
|
|
2249
2261
|
}
|
|
2250
|
-
.rounded-t
|
|
2251
|
-
border-top-left-radius:
|
|
2252
|
-
border-top-right-radius:
|
|
2262
|
+
.rounded-t-\[10px\] {
|
|
2263
|
+
border-top-left-radius: 10px;
|
|
2264
|
+
border-top-right-radius: 10px;
|
|
2253
2265
|
}
|
|
2254
2266
|
.rounded-t-lg {
|
|
2255
2267
|
border-top-left-radius: var(--radius-lg);
|
|
@@ -2259,9 +2271,9 @@
|
|
|
2259
2271
|
border-top-left-radius: var(--radius-md);
|
|
2260
2272
|
border-top-right-radius: var(--radius-md);
|
|
2261
2273
|
}
|
|
2262
|
-
.rounded-l
|
|
2263
|
-
border-top-left-radius:
|
|
2264
|
-
border-bottom-left-radius:
|
|
2274
|
+
.rounded-l-\[10px\] {
|
|
2275
|
+
border-top-left-radius: 10px;
|
|
2276
|
+
border-bottom-left-radius: 10px;
|
|
2265
2277
|
}
|
|
2266
2278
|
.rounded-l-lg {
|
|
2267
2279
|
border-top-left-radius: var(--radius-lg);
|
|
@@ -2274,9 +2286,9 @@
|
|
|
2274
2286
|
.rounded-tl-lg {
|
|
2275
2287
|
border-top-left-radius: var(--radius-lg);
|
|
2276
2288
|
}
|
|
2277
|
-
.rounded-r
|
|
2278
|
-
border-top-right-radius:
|
|
2279
|
-
border-bottom-right-radius:
|
|
2289
|
+
.rounded-r-\[10px\] {
|
|
2290
|
+
border-top-right-radius: 10px;
|
|
2291
|
+
border-bottom-right-radius: 10px;
|
|
2280
2292
|
}
|
|
2281
2293
|
.rounded-r-lg {
|
|
2282
2294
|
border-top-right-radius: var(--radius-lg);
|
|
@@ -2289,9 +2301,9 @@
|
|
|
2289
2301
|
.rounded-tr-lg {
|
|
2290
2302
|
border-top-right-radius: var(--radius-lg);
|
|
2291
2303
|
}
|
|
2292
|
-
.rounded-b
|
|
2293
|
-
border-bottom-right-radius:
|
|
2294
|
-
border-bottom-left-radius:
|
|
2304
|
+
.rounded-b-\[10px\] {
|
|
2305
|
+
border-bottom-right-radius: 10px;
|
|
2306
|
+
border-bottom-left-radius: 10px;
|
|
2295
2307
|
}
|
|
2296
2308
|
.rounded-b-lg {
|
|
2297
2309
|
border-bottom-right-radius: var(--radius-lg);
|
|
@@ -2521,6 +2533,9 @@
|
|
|
2521
2533
|
.bg-neutral100 {
|
|
2522
2534
|
background-color: var(--color-neutral100);
|
|
2523
2535
|
}
|
|
2536
|
+
.bg-neutral100\! {
|
|
2537
|
+
background-color: var(--color-neutral100) !important;
|
|
2538
|
+
}
|
|
2524
2539
|
.bg-neutral200 {
|
|
2525
2540
|
background-color: var(--color-neutral200);
|
|
2526
2541
|
}
|
|
@@ -2642,6 +2657,9 @@
|
|
|
2642
2657
|
.p-0 {
|
|
2643
2658
|
padding: calc(var(--spacing) * 0);
|
|
2644
2659
|
}
|
|
2660
|
+
.p-0\.5 {
|
|
2661
|
+
padding: calc(var(--spacing) * 0.5);
|
|
2662
|
+
}
|
|
2645
2663
|
.p-0\.75 {
|
|
2646
2664
|
padding: calc(var(--spacing) * 0.75);
|
|
2647
2665
|
}
|
|
@@ -2789,9 +2807,6 @@
|
|
|
2789
2807
|
.pt-1 {
|
|
2790
2808
|
padding-top: calc(var(--spacing) * 1);
|
|
2791
2809
|
}
|
|
2792
|
-
.pt-2 {
|
|
2793
|
-
padding-top: calc(var(--spacing) * 2);
|
|
2794
|
-
}
|
|
2795
2810
|
.pt-4 {
|
|
2796
2811
|
padding-top: calc(var(--spacing) * 4);
|
|
2797
2812
|
}
|
|
@@ -3240,6 +3255,9 @@
|
|
|
3240
3255
|
.opacity-100 {
|
|
3241
3256
|
opacity: 100%;
|
|
3242
3257
|
}
|
|
3258
|
+
.opacity-100\! {
|
|
3259
|
+
opacity: 100% !important;
|
|
3260
|
+
}
|
|
3243
3261
|
.shadow {
|
|
3244
3262
|
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
|
3245
3263
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
@@ -3637,6 +3655,12 @@
|
|
|
3637
3655
|
inset-block: calc(var(--spacing) * 0);
|
|
3638
3656
|
}
|
|
3639
3657
|
}
|
|
3658
|
+
.after\:top-1\/2 {
|
|
3659
|
+
&::after {
|
|
3660
|
+
content: var(--tw-content);
|
|
3661
|
+
top: calc(1 / 2 * 100%);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3640
3664
|
.after\:top-full {
|
|
3641
3665
|
&::after {
|
|
3642
3666
|
content: var(--tw-content);
|
|
@@ -3649,12 +3673,25 @@
|
|
|
3649
3673
|
right: 100%;
|
|
3650
3674
|
}
|
|
3651
3675
|
}
|
|
3676
|
+
.after\:left-1\/2 {
|
|
3677
|
+
&::after {
|
|
3678
|
+
content: var(--tw-content);
|
|
3679
|
+
left: calc(1 / 2 * 100%);
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3652
3682
|
.after\:left-full {
|
|
3653
3683
|
&::after {
|
|
3654
3684
|
content: var(--tw-content);
|
|
3655
3685
|
left: 100%;
|
|
3656
3686
|
}
|
|
3657
3687
|
}
|
|
3688
|
+
.after\:size-10 {
|
|
3689
|
+
&::after {
|
|
3690
|
+
content: var(--tw-content);
|
|
3691
|
+
width: calc(var(--spacing) * 10);
|
|
3692
|
+
height: calc(var(--spacing) * 10);
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3658
3695
|
.after\:h-0\.5 {
|
|
3659
3696
|
&::after {
|
|
3660
3697
|
content: var(--tw-content);
|
|
@@ -3667,6 +3704,14 @@
|
|
|
3667
3704
|
width: calc(var(--spacing) * 0.5);
|
|
3668
3705
|
}
|
|
3669
3706
|
}
|
|
3707
|
+
.after\:-translate-1\/2 {
|
|
3708
|
+
&::after {
|
|
3709
|
+
content: var(--tw-content);
|
|
3710
|
+
--tw-translate-x: calc(calc(1 / 2 * 100%) * -1);
|
|
3711
|
+
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
|
|
3712
|
+
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3670
3715
|
.after\:bg-transparent {
|
|
3671
3716
|
&::after {
|
|
3672
3717
|
content: var(--tw-content);
|
|
@@ -4678,21 +4723,6 @@
|
|
|
4678
4723
|
visibility: hidden;
|
|
4679
4724
|
}
|
|
4680
4725
|
}
|
|
4681
|
-
.mobile\:absolute {
|
|
4682
|
-
@media (max-width: calc(768px - 1px)) {
|
|
4683
|
-
position: absolute;
|
|
4684
|
-
}
|
|
4685
|
-
}
|
|
4686
|
-
.mobile\:top-1\/2 {
|
|
4687
|
-
@media (max-width: calc(768px - 1px)) {
|
|
4688
|
-
top: calc(1 / 2 * 100%);
|
|
4689
|
-
}
|
|
4690
|
-
}
|
|
4691
|
-
.mobile\:left-1\/2 {
|
|
4692
|
-
@media (max-width: calc(768px - 1px)) {
|
|
4693
|
-
left: calc(1 / 2 * 100%);
|
|
4694
|
-
}
|
|
4695
|
-
}
|
|
4696
4726
|
.mobile\:block {
|
|
4697
4727
|
@media (max-width: calc(768px - 1px)) {
|
|
4698
4728
|
display: block;
|
|
@@ -4748,13 +4778,6 @@
|
|
|
4748
4778
|
flex: 1;
|
|
4749
4779
|
}
|
|
4750
4780
|
}
|
|
4751
|
-
.mobile\:-translate-1\/2 {
|
|
4752
|
-
@media (max-width: calc(768px - 1px)) {
|
|
4753
|
-
--tw-translate-x: calc(calc(1 / 2 * 100%) * -1);
|
|
4754
|
-
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
|
|
4755
|
-
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
4756
|
-
}
|
|
4757
|
-
}
|
|
4758
4781
|
.mobile\:flex-col {
|
|
4759
4782
|
@media (max-width: calc(768px - 1px)) {
|
|
4760
4783
|
flex-direction: column;
|
|
@@ -4841,6 +4864,14 @@
|
|
|
4841
4864
|
font-weight: var(--font-medium);
|
|
4842
4865
|
}
|
|
4843
4866
|
}
|
|
4867
|
+
.tablet\:size-5 {
|
|
4868
|
+
@media (min-width: 768px) {
|
|
4869
|
+
@media (max-width: calc(992px - 1px)) {
|
|
4870
|
+
width: calc(var(--spacing) * 5);
|
|
4871
|
+
height: calc(var(--spacing) * 5);
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4844
4875
|
.tablet-down\:display12 {
|
|
4845
4876
|
@media (max-width: calc(992px - 1px)) {
|
|
4846
4877
|
font-size: var(--text-display12);
|
|
@@ -4862,6 +4893,12 @@
|
|
|
4862
4893
|
letter-spacing: var(--tracking-display16);
|
|
4863
4894
|
}
|
|
4864
4895
|
}
|
|
4896
|
+
.pc\:size-6 {
|
|
4897
|
+
@media (min-width: 992px) {
|
|
4898
|
+
width: calc(var(--spacing) * 6);
|
|
4899
|
+
height: calc(var(--spacing) * 6);
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4865
4902
|
.\[\&_svg\]\:icon-2xs {
|
|
4866
4903
|
& svg {
|
|
4867
4904
|
width: var(--control-icon-2xs);
|
|
@@ -5534,7 +5571,7 @@
|
|
|
5534
5571
|
}
|
|
5535
5572
|
.ProseMirror img {
|
|
5536
5573
|
max-width: 100%;
|
|
5537
|
-
height: auto;
|
|
5574
|
+
height: auto !important;
|
|
5538
5575
|
display: block;
|
|
5539
5576
|
margin: 1rem auto;
|
|
5540
5577
|
}
|
|
@@ -5552,7 +5589,6 @@
|
|
|
5552
5589
|
}
|
|
5553
5590
|
.ProseMirror img {
|
|
5554
5591
|
cursor: pointer;
|
|
5555
|
-
border: 2px solid transparent;
|
|
5556
5592
|
transition: border-color 0.2s;
|
|
5557
5593
|
}
|
|
5558
5594
|
.ProseMirror img:hover {
|
|
@@ -5621,6 +5657,9 @@
|
|
|
5621
5657
|
.ProseMirror [data-resize-wrapper] img {
|
|
5622
5658
|
margin: 0 !important;
|
|
5623
5659
|
}
|
|
5660
|
+
.ProseMirror [data-resize-wrapper] {
|
|
5661
|
+
width: fit-content;
|
|
5662
|
+
}
|
|
5624
5663
|
.ProseMirror .tableWrapper {
|
|
5625
5664
|
overflow-x: auto;
|
|
5626
5665
|
margin: 1rem 0;
|
package/package.json
CHANGED
|
@@ -47,9 +47,13 @@
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/* 이미지 정렬 스타일 */
|
|
50
|
+
/* height만 !important로 잠근다. 리사이즈 노드뷰가 width/height를 함께 인라인 px로
|
|
51
|
+
쓰는데, 높이가 고정되면 컨테이너가 좁아질 때 max-width로 폭만 줄어 이미지가
|
|
52
|
+
찌그러진다. 높이를 auto로 두면 항상 폭을 따라가므로 비율이 유지되고,
|
|
53
|
+
width는 인라인이 그대로 먹으므로 드래그 리사이즈도 동작한다. */
|
|
50
54
|
.ProseMirror img {
|
|
51
55
|
max-width: 100%;
|
|
52
|
-
height: auto;
|
|
56
|
+
height: auto !important;
|
|
53
57
|
display: block;
|
|
54
58
|
margin: 1rem auto;
|
|
55
59
|
}
|
|
@@ -72,7 +76,6 @@
|
|
|
72
76
|
/* 이미지 선택 스타일 */
|
|
73
77
|
.ProseMirror img {
|
|
74
78
|
cursor: pointer;
|
|
75
|
-
border: 2px solid transparent;
|
|
76
79
|
transition: border-color 0.2s;
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -166,6 +169,13 @@
|
|
|
166
169
|
margin: 0 !important;
|
|
167
170
|
}
|
|
168
171
|
|
|
172
|
+
/* 리사이즈 핸들은 래퍼를 기준으로 배치된다. 래퍼가 이미지보다 넓으면 핸들이
|
|
173
|
+
이미지 모서리에서 떨어져 보이므로, 래퍼는 항상 이미지 크기에 맞춘다.
|
|
174
|
+
(래퍼는 부모가 block이면 기본값이 100%라 가만두면 벌어진다.) */
|
|
175
|
+
.ProseMirror [data-resize-wrapper] {
|
|
176
|
+
width: fit-content;
|
|
177
|
+
}
|
|
178
|
+
|
|
169
179
|
/* 표
|
|
170
180
|
prose 기본 표는 가로줄만 그어 에디터에서 셀 경계가 보이지 않는다. 격자 테두리로 덮는다.
|
|
171
181
|
@layer 밖(=무레이어)이라 utilities 레이어의 prose보다 우선한다. */
|