@loword/loword-design-system 0.2.1 → 0.2.3
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/LDDataCalendar/LDDataCalendar.js +1 -1
- package/dist/components/LDDatePicker/LDDatePicker.js +1 -1
- package/dist/components/LDDateRangePicker/LDDateRangePicker.js +2 -2
- package/dist/components/LDEditor/EditorBase.d.ts.map +1 -1
- package/dist/components/LDEditor/EditorBase.js +25 -2
- package/dist/components/LDEditor/LDEditor.d.ts.map +1 -1
- package/dist/components/LDEditor/LDEditor.js +2 -1
- package/dist/components/LDFileUpload/FileUploadBase.d.ts +20 -4
- package/dist/components/LDFileUpload/FileUploadBase.d.ts.map +1 -1
- package/dist/components/LDFileUpload/FileUploadBase.js +41 -18
- package/dist/components/LDFileUpload/LDFileUpload.d.ts +19 -6
- package/dist/components/LDFileUpload/LDFileUpload.d.ts.map +1 -1
- package/dist/components/LDFileUpload/LDFileUpload.js +124 -28
- package/dist/components/LDFileUpload/LDFileUploadList.js +1 -1
- package/dist/components/LDInput/LDInput.d.ts +3 -1
- package/dist/components/LDInput/LDInput.d.ts.map +1 -1
- package/dist/components/LDInput/LDInput.js +10 -4
- package/dist/components/LDLNB/LDLNB.d.ts.map +1 -1
- package/dist/components/LDLNB/LDLNB.js +1 -2
- package/dist/components/LDLNB/LNBBase.d.ts +1 -2
- package/dist/components/LDLNB/LNBBase.d.ts.map +1 -1
- package/dist/components/LDLNB/LNBBase.js +10 -4
- package/dist/components/LDModal/LDModal.d.ts +7 -6
- package/dist/components/LDModal/LDModal.d.ts.map +1 -1
- package/dist/components/LDModal/LDModalView.d.ts +3 -3
- package/dist/components/LDModal/LDModalView.d.ts.map +1 -1
- package/dist/components/LDModal/LDModalView.js +6 -5
- package/dist/components/LDSelect/LDMultiSelect.js +1 -1
- package/dist/components/LDSelect/LDSelect.js +1 -1
- package/dist/components/LDTable/HeadlessTable.d.ts +4 -0
- package/dist/components/LDTable/HeadlessTable.d.ts.map +1 -1
- package/dist/components/LDTable/HeadlessTable.js +12 -1
- package/dist/components/LDTable/LDCollapsibleTanstackTable.d.ts.map +1 -1
- package/dist/components/LDTable/LDCollapsibleTanstackTable.js +2 -2
- package/dist/components/LDTable/LDTable.js +2 -2
- package/dist/components/LDTable/LDTanstackTable.d.ts.map +1 -1
- package/dist/components/LDTable/LDTanstackTable.js +2 -2
- package/dist/components/LDTextarea/LDTextarea.js +7 -7
- package/dist/components/LDTimePicker/LDTimePicker.js +1 -1
- package/dist/index.css +231 -21
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/utils/file.d.ts +2 -0
- package/dist/utils/file.d.ts.map +1 -1
- package/dist/utils/file.js +1 -0
- package/dist/utils/utils.d.ts +29 -3
- package/dist/utils/utils.d.ts.map +1 -1
- package/dist/utils/utils.js +93 -4
- package/package.json +1 -1
- package/src/styles/components/editor.css +14 -0
|
@@ -17,7 +17,7 @@ const Next = ({ children, className, ...props }) => (_jsx(LDDataCalendarBase.Nex
|
|
|
17
17
|
/** 요일과 날짜 셀을 7열로 렌더링하는 그리드입니다. */
|
|
18
18
|
const Grid = ({ weekdayClassName, ...props }) => (_jsx(LDDataCalendarBase.Grid, { weekdayClassName: cn('py-[10px] display14 text-mono500 mobile:py-1 mobile:display12', weekdayClassName), ...props }));
|
|
19
19
|
/** 날짜와 해당 날짜의 데이터를 표시하는 셀입니다. */
|
|
20
|
-
const Cell = ({ className, dateClassName, ...props }) => (_jsx(LDDataCalendarBase.Cell, { className: cn('flex min-h-
|
|
20
|
+
const Cell = ({ className, dateClassName, ...props }) => (_jsx(LDDataCalendarBase.Cell, { className: cn('flex min-h-25 flex-col gap-[15px] py-[10px] data-outside:opacity-50 mobile:min-h-13 mobile:gap-1 mobile:py-1', 'ld-transition data-selectable:cursor-pointer data-selectable:rounded-[5px] data-selectable:hover:bg-mono100', 'data-[selected=true]:bg-tintBlue data-[selected=true]:hover:bg-tintBlue', className), dateClassName: cn('self-start display16 leading-none font-normal data-weekend:text-danger600 mobile:display12', dateClassName), ...props }));
|
|
21
21
|
/** 선택한 날짜의 상세 내용을 표시하는 모바일 영역입니다. */
|
|
22
22
|
const Detail = ({ className, ...props }) => (_jsx(LDDataCalendarBase.Detail, { className: cn('mt-md hidden rounded-[10px] bg-mono50 p-md mobile:block', className), ...props }));
|
|
23
23
|
/**
|
|
@@ -102,6 +102,6 @@ const LDDatePicker = ({ variant = 'filled', color = 'neutral', value, defaultVal
|
|
|
102
102
|
setSelectedDate(value);
|
|
103
103
|
setSelectedTime(toTimeString(value));
|
|
104
104
|
}, [open, value]);
|
|
105
|
-
return (_jsx("div", { className: cn(block ? 'w-full' : 'w-auto', className), children: _jsxs(PopoverBase.Root, { open: open, onOpenChange: handleOpenChange, children: [_jsxs("div", { style: style, className: cn('relative h-9.5 w-full cursor-pointer rounded-[5px] px-4 display14', 'flex flex-row items-center justify-between gap-4', 'bold outline-2 outline-transparent ld-transition', variantColorClasses[variant][color], disabled && 'cursor-not-allowed opacity-50', triggerClassName), children: [_jsx("span", { "aria-hidden": true, className: cn('truncate', !displayDate && 'text-
|
|
105
|
+
return (_jsx("div", { className: cn(block ? 'w-full' : 'w-auto', className), children: _jsxs(PopoverBase.Root, { open: open, onOpenChange: handleOpenChange, children: [_jsxs("div", { style: style, className: cn('relative h-9.5 w-full cursor-pointer rounded-[5px] px-4 display14', 'flex flex-row items-center justify-between gap-4', 'bold outline-2 outline-transparent ld-transition', variantColorClasses[variant][color], disabled && 'cursor-not-allowed opacity-50', triggerClassName), children: [_jsx("span", { "aria-hidden": true, className: cn('truncate', !displayDate && 'text-neutral300'), children: displayDate ? formattedDate : placeholderText }), _jsx(PopoverBase.Trigger, { "aria-label": displayDate ? formattedDate : placeholderText, disabled: disabled, className: cn('absolute inset-0 rounded-[5px] outline-none', disabled ? 'cursor-not-allowed' : 'cursor-pointer') }), _jsxs("div", { className: "pointer-events-none relative flex shrink-0 items-center gap-xs", children: [showRotate && (_jsx("button", { type: "button", "aria-label": "\uCD08\uAE30\uD654", disabled: disabled, onClick: handleReset, className: "pointer-events-auto flex cursor-pointer items-center text-mono400 ld-transition disabled:cursor-not-allowed", children: _jsx(RotateCw, { size: 15 }) })), trailingIcon] })] }), _jsx(PopoverBase.Content, { className: "rounded-md border border-neutral200 bg-white p-0 shadow-md", children: _jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex max-h-90 flex-row", children: [_jsx(LDCalendar, { captionLayout: "dropdown", mode: "single", locale: ko, selected: selectedDate, onSelect: handleSelect, disabled: isDateDisabled, colorSet: colorSet }), isExistTime && (_jsx(CalendarTimePicker, { value: selectedTime, onChange: handleSelectedTime, colorSet: colorSet, className: "shrink-0" }))] }), _jsxs("div", { className: "flex items-center justify-end gap-sm border-t border-neutral100 py-xs px-sm", children: [_jsx(LDButton, { variant: "outline", color: "neutral", onClick: handleCancel, children: "\uCDE8\uC18C" }), _jsx(LDButton, { onClick: handleConfirm, children: "\uD655\uC778" })] })] }) })] }) }));
|
|
106
106
|
};
|
|
107
107
|
export default LDDatePicker;
|
|
@@ -85,10 +85,10 @@ const LDDateRangePicker = ({ variant = 'filled', value, onChange, disabled = fal
|
|
|
85
85
|
})();
|
|
86
86
|
const renderDateRange = () => {
|
|
87
87
|
if (!displayRange?.from) {
|
|
88
|
-
return (_jsx("span", { className: "whitespace-nowrap text-
|
|
88
|
+
return (_jsx("span", { className: "whitespace-nowrap text-neutral300", children: placeholderText }));
|
|
89
89
|
}
|
|
90
90
|
if (!displayRange.to) {
|
|
91
|
-
return (_jsxs("span", { className: "text-
|
|
91
|
+
return (_jsxs("span", { className: "text-neutral300", children: [dayjs(displayRange.from).format(displayFmt), " ~ ", placeholderFmt] }));
|
|
92
92
|
}
|
|
93
93
|
return (_jsxs(_Fragment, { children: [dayjs(displayRange.from).format(displayFmt), " ~", ' ', dayjs(displayRange.to).format(displayFmt)] }));
|
|
94
94
|
};
|
|
@@ -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,
|
|
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;AAwCD,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;AA0ED,UAAU,aAAa;IACrB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgBD,eAAO,MAAM,UAAU;oEA9LpB,SAAS;6BAoCoB,YAAY;wCAWE,YAAY;yFA2BvD,kBAAkB;sDA4BlB,oBAAoB;;mDA0E+B,aAAa;CAmBlE,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { EditorContent, useEditor, } from '@tiptap/react';
|
|
4
|
-
import { createContext, useContext, useEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, } from 'react';
|
|
5
5
|
import cn from '../../utils/cn.js';
|
|
6
6
|
const EditorContext = createContext(null);
|
|
7
7
|
export const useEditorContext = () => {
|
|
@@ -51,7 +51,10 @@ const ToolbarButton = ({ disabled, onClick, className, children, 'aria-label': a
|
|
|
51
51
|
};
|
|
52
52
|
const ToolbarDropdown = ({ trigger, className, children, }) => {
|
|
53
53
|
const [open, setOpen] = useState(false);
|
|
54
|
+
// 패널 기준 위치는 트리거 왼쪽. 툴바(=에디터 폭) 오른쪽을 넘는 만큼만 왼쪽으로 밀어 넣는다.
|
|
55
|
+
const [shift, setShift] = useState(0);
|
|
54
56
|
const ref = useRef(null);
|
|
57
|
+
const panelRef = useRef(null);
|
|
55
58
|
const toggle = () => setOpen((prev) => !prev);
|
|
56
59
|
const close = () => setOpen(false);
|
|
57
60
|
useEffect(() => {
|
|
@@ -63,7 +66,27 @@ const ToolbarDropdown = ({ trigger, className, children, }) => {
|
|
|
63
66
|
document.addEventListener('mousedown', handler);
|
|
64
67
|
return () => document.removeEventListener('mousedown', handler);
|
|
65
68
|
}, []);
|
|
66
|
-
|
|
69
|
+
// paint 전에 위치를 확정해야 열리는 첫 프레임이 튀지 않는다.
|
|
70
|
+
// Root가 editor 생성 전엔 children을 렌더하지 않으므로 서버에서는 실행되지 않는다.
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
if (!open)
|
|
73
|
+
return;
|
|
74
|
+
const measure = () => {
|
|
75
|
+
const bounds = ref.current?.parentElement;
|
|
76
|
+
const panel = panelRef.current;
|
|
77
|
+
if (!ref.current || !bounds || !panel)
|
|
78
|
+
return;
|
|
79
|
+
const boundsRect = bounds.getBoundingClientRect();
|
|
80
|
+
const triggerRect = ref.current.getBoundingClientRect();
|
|
81
|
+
const overflow = triggerRect.left + panel.offsetWidth - boundsRect.right;
|
|
82
|
+
// 왼쪽 경계도 넘지 않도록 밀 수 있는 양을 제한한다.
|
|
83
|
+
setShift(Math.max(0, Math.min(overflow, triggerRect.left - boundsRect.left)));
|
|
84
|
+
};
|
|
85
|
+
measure();
|
|
86
|
+
window.addEventListener('resize', measure);
|
|
87
|
+
return () => window.removeEventListener('resize', measure);
|
|
88
|
+
}, [open]);
|
|
89
|
+
return (_jsxs("div", { ref: ref, className: "relative", 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 }))] }));
|
|
67
90
|
};
|
|
68
91
|
const Toolbar = Object.assign(ToolbarRoot, {
|
|
69
92
|
LDButton: ToolbarButton,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LDEditor.d.ts","sourceRoot":"","sources":["../../../src/components/LDEditor/LDEditor.tsx"],"names":[],"mappings":"AA4EA,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,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;CAC9B;AAED,QAAA,MAAM,QAAQ,GAAI,sDAKf,aAAa,4CAYf,CAAC;
|
|
1
|
+
{"version":3,"file":"LDEditor.d.ts","sourceRoot":"","sources":["../../../src/components/LDEditor/LDEditor.tsx"],"names":[],"mappings":"AA4EA,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,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;CAC9B;AAED,QAAA,MAAM,QAAQ,GAAI,sDAKf,aAAa,4CAYf,CAAC;AAyjBF,eAAe,QAAQ,CAAC"}
|
|
@@ -77,6 +77,7 @@ const EditorToolbar = ({ handleImageUpload, }) => {
|
|
|
77
77
|
canUndo: editor.can().undo(),
|
|
78
78
|
canRedo: editor.can().redo(),
|
|
79
79
|
isLink: editor.isActive('link'),
|
|
80
|
+
hasSelection: !editor.state.selection.empty,
|
|
80
81
|
};
|
|
81
82
|
},
|
|
82
83
|
});
|
|
@@ -148,7 +149,7 @@ const EditorToolbar = ({ handleImageUpload, }) => {
|
|
|
148
149
|
justify: '양쪽 정렬',
|
|
149
150
|
}[align];
|
|
150
151
|
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));
|
|
151
|
-
})] })), _jsx(LDDivider, {}), _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, {}), _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" }) }), !isMobile && (_jsxs(_Fragment, { children: [_jsx(LDDivider, {}), _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, { onClick: () => {
|
|
152
|
+
})] })), _jsx(LDDivider, {}), _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, {}), _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" }) }), !isMobile && (_jsxs(_Fragment, { children: [_jsx(LDDivider, {}), _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: () => {
|
|
152
153
|
setLinkUrl(editor.getAttributes('link').href || '');
|
|
153
154
|
toggle();
|
|
154
155
|
}, className: cn(iconBtn, state.isLink && activeBtn), "aria-label": "\uB9C1\uD06C", children: _jsx(Link2, { className: "size-5" }) })), children: ({ close }) => {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type Dispatch, type HTMLAttributes, type InputHTMLAttributes, type ReactNode, type Ref, type SetStateAction } from 'react';
|
|
2
2
|
import { type TMimeType } from '../../constants/file.js';
|
|
3
3
|
import { type IFile } from '../../utils/file.js';
|
|
4
|
+
/** 인풋마다 다르게 줄 수 있는 파일 허용 조건입니다. 생략한 항목은 Root 값을 씁니다. */
|
|
5
|
+
export interface FileUploadOptions {
|
|
6
|
+
maxSize?: number;
|
|
7
|
+
allowedExtensions?: TMimeType[];
|
|
8
|
+
maxFileCount?: number;
|
|
9
|
+
}
|
|
4
10
|
interface FileUploadContextValue {
|
|
5
11
|
recommendedSize?: {
|
|
6
12
|
width: number;
|
|
@@ -9,13 +15,17 @@ interface FileUploadContextValue {
|
|
|
9
15
|
maxSize: number;
|
|
10
16
|
allowedExtensions: TMimeType[];
|
|
11
17
|
fileList: IFile[];
|
|
18
|
+
linkList: string[];
|
|
12
19
|
maxFileCount: number;
|
|
13
20
|
disabled?: boolean;
|
|
14
21
|
readOnly?: boolean;
|
|
15
22
|
locked: boolean;
|
|
16
|
-
addFiles: (files: File[]) => Promise<void>;
|
|
17
|
-
replaceFile: (file: File, index: number) => Promise<void>;
|
|
23
|
+
addFiles: (files: File[], options?: FileUploadOptions) => Promise<void>;
|
|
24
|
+
replaceFile: (file: File, index: number, options?: FileUploadOptions) => Promise<void>;
|
|
18
25
|
deleteFile: (index: number) => Promise<void>;
|
|
26
|
+
addLink: () => Promise<void>;
|
|
27
|
+
changeLink: (index: number, value: string) => Promise<void>;
|
|
28
|
+
deleteLink: (index: number) => Promise<void>;
|
|
19
29
|
}
|
|
20
30
|
export declare const useFileUploadContext: () => FileUploadContextValue;
|
|
21
31
|
export interface FileUploadBaseRootProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
|
|
@@ -27,6 +37,10 @@ export interface FileUploadBaseRootProps extends Omit<HTMLAttributes<HTMLDivElem
|
|
|
27
37
|
allowedExtensions?: TMimeType[];
|
|
28
38
|
fileList: IFile[];
|
|
29
39
|
setFileList: Dispatch<SetStateAction<IFile[]>>;
|
|
40
|
+
/** 첨부된 링크 목록입니다. `Input type="link"`·`LinkList`를 쓸 때 넘깁니다. */
|
|
41
|
+
linkList?: string[];
|
|
42
|
+
setLinkList?: Dispatch<SetStateAction<string[]>>;
|
|
43
|
+
onChangeLink?: (links: string[]) => void | Promise<void>;
|
|
30
44
|
maxFileCount: number;
|
|
31
45
|
onChangeFile: (files: IFile[]) => void | Promise<void>;
|
|
32
46
|
onDeleteFile: (file: IFile) => Promise<boolean>;
|
|
@@ -37,12 +51,14 @@ export interface FileUploadBaseRootProps extends Omit<HTMLAttributes<HTMLDivElem
|
|
|
37
51
|
}
|
|
38
52
|
export interface FileUploadBaseInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'multiple' | 'onChange' | 'type'> {
|
|
39
53
|
replaceIndex?: number;
|
|
54
|
+
/** 이 인풋에만 적용할 파일 허용 조건입니다. 생략한 항목은 Root 값을 씁니다. */
|
|
55
|
+
options?: FileUploadOptions;
|
|
40
56
|
onChange?: InputHTMLAttributes<HTMLInputElement>['onChange'];
|
|
41
57
|
ref?: Ref<HTMLInputElement>;
|
|
42
58
|
}
|
|
43
59
|
export declare const FileUploadBase: {
|
|
44
|
-
Root: ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsProp, maxFileCount, fileList, setFileList, onChangeFile, onDeleteFile, disabled, readOnly, children, ref, ...props }: FileUploadBaseRootProps) => import("react/jsx-runtime").JSX.Element;
|
|
45
|
-
Input: ({ replaceIndex, onClick, onChange, disabled, ref, ...props }: FileUploadBaseInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
60
|
+
Root: ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsProp, maxFileCount, fileList, setFileList, linkList: linkListProp, setLinkList, onChangeLink, onChangeFile, onDeleteFile, disabled, readOnly, children, ref, ...props }: FileUploadBaseRootProps) => import("react/jsx-runtime").JSX.Element;
|
|
61
|
+
Input: ({ replaceIndex, options, onClick, onChange, disabled, ref, ...props }: FileUploadBaseInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
46
62
|
};
|
|
47
63
|
export default FileUploadBase;
|
|
48
64
|
//# sourceMappingURL=FileUploadBase.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileUploadBase.d.ts","sourceRoot":"","sources":["../../../src/components/LDFileUpload/FileUploadBase.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,GAAG,EACR,KAAK,cAAc,EAEpB,MAAM,OAAO,CAAC;AAGf,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAGL,KAAK,KAAK,EAKX,MAAM,cAAc,CAAC;AAEtB,UAAU,sBAAsB;IAC9B,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,SAAS,EAAE,CAAC;IAC/B,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"FileUploadBase.d.ts","sourceRoot":"","sources":["../../../src/components/LDFileUpload/FileUploadBase.tsx"],"names":[],"mappings":"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"}
|
|
@@ -12,29 +12,38 @@ export const useFileUploadContext = () => {
|
|
|
12
12
|
}
|
|
13
13
|
return context;
|
|
14
14
|
};
|
|
15
|
-
const Root = ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsProp, maxFileCount, fileList, setFileList, onChangeFile, onDeleteFile, disabled, readOnly, children, ref, ...props }) => {
|
|
15
|
+
const Root = ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsProp, maxFileCount, fileList, setFileList, linkList: linkListProp, setLinkList, onChangeLink, onChangeFile, onDeleteFile, disabled, readOnly, children, ref, ...props }) => {
|
|
16
16
|
const allowedExtensions = allowedExtensionsProp?.length
|
|
17
17
|
? allowedExtensionsProp
|
|
18
18
|
: SUPPORTED_FILE_MIME_TYPES;
|
|
19
19
|
const locked = Boolean(disabled || readOnly);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const linkList = linkListProp ?? [];
|
|
21
|
+
// 인풋마다 허용 조건이 다를 수 있어, Root 값 위에 인풋이 준 옵션을 덮어쓴다.
|
|
22
|
+
const resolveOptions = (options) => ({
|
|
23
|
+
maxSize: options?.maxSize ?? maxSize,
|
|
24
|
+
allowedExtensions: options?.allowedExtensions?.length
|
|
25
|
+
? options.allowedExtensions
|
|
26
|
+
: allowedExtensions,
|
|
27
|
+
maxFileCount: options?.maxFileCount ?? maxFileCount,
|
|
28
|
+
});
|
|
29
|
+
const validateFile = async (file, resolved) => {
|
|
30
|
+
if (!validAllowedExtensions(file, resolved.allowedExtensions)) {
|
|
31
|
+
const allowed = resolved.allowedExtensions
|
|
23
32
|
.map((item) => FILE_EXTENSIONS[item].toUpperCase())
|
|
24
33
|
.join(', ');
|
|
25
34
|
return `지원하지 않는 파일 형식입니다. ${allowed} 파일만 업로드 가능합니다.`;
|
|
26
35
|
}
|
|
27
|
-
if (!validFileSize(file, maxSize)) {
|
|
28
|
-
return `파일 크기가 너무 큽니다. ${getFileSizeUnit(maxSize)} 이하 파일을 선택해주세요.`;
|
|
36
|
+
if (!validFileSize(file, resolved.maxSize)) {
|
|
37
|
+
return `파일 크기가 너무 큽니다. ${getFileSizeUnit(resolved.maxSize)} 이하 파일을 선택해주세요.`;
|
|
29
38
|
}
|
|
30
39
|
if (isImage(file.name) && !(await validImageSize(file))) {
|
|
31
40
|
return '이미지 해상도가 너무 큽니다. 더 작은 크기의 이미지를 선택해주세요.';
|
|
32
41
|
}
|
|
33
42
|
return null;
|
|
34
43
|
};
|
|
35
|
-
const convertFiles = async (files) => {
|
|
44
|
+
const convertFiles = async (files, resolved) => {
|
|
36
45
|
for (const file of files) {
|
|
37
|
-
const error = await validateFile(file);
|
|
46
|
+
const error = await validateFile(file, resolved);
|
|
38
47
|
if (error) {
|
|
39
48
|
LDToast.error(error);
|
|
40
49
|
return null;
|
|
@@ -50,20 +59,21 @@ const Root = ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsPr
|
|
|
50
59
|
return null;
|
|
51
60
|
}
|
|
52
61
|
};
|
|
53
|
-
const addFiles = async (files) => {
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
const addFiles = async (files, options) => {
|
|
63
|
+
const resolved = resolveOptions(options);
|
|
64
|
+
if (fileList.length + files.length > resolved.maxFileCount) {
|
|
65
|
+
LDToast.error(`최대 ${resolved.maxFileCount}개까지 업로드할 수 있습니다.`);
|
|
56
66
|
return;
|
|
57
67
|
}
|
|
58
|
-
const converted = await convertFiles(files);
|
|
68
|
+
const converted = await convertFiles(files, resolved);
|
|
59
69
|
if (!converted)
|
|
60
70
|
return;
|
|
61
71
|
const next = [...fileList, ...converted];
|
|
62
72
|
setFileList(next);
|
|
63
73
|
await onChangeFile(next);
|
|
64
74
|
};
|
|
65
|
-
const replaceFile = async (file, index) => {
|
|
66
|
-
const [converted] = (await convertFiles([file])) ?? [];
|
|
75
|
+
const replaceFile = async (file, index, options) => {
|
|
76
|
+
const [converted] = (await convertFiles([file], resolveOptions(options))) ?? [];
|
|
67
77
|
if (!converted)
|
|
68
78
|
return;
|
|
69
79
|
const next = fileList.map((item, itemIndex) => itemIndex === index ? converted : item);
|
|
@@ -85,11 +95,20 @@ const Root = ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsPr
|
|
|
85
95
|
LDToast.error('파일 삭제 중 문제가 발생했습니다. 잠시후 다시 시도해주세요.');
|
|
86
96
|
}
|
|
87
97
|
};
|
|
98
|
+
// 링크는 파일이 아니라 검증·변환 대상이 아니므로 목록 갱신만 하고 onChangeLink로 알린다.
|
|
99
|
+
const applyLinks = async (next) => {
|
|
100
|
+
setLinkList?.(next);
|
|
101
|
+
await onChangeLink?.(next);
|
|
102
|
+
};
|
|
103
|
+
const addLink = () => applyLinks([...linkList, '']);
|
|
104
|
+
const changeLink = (index, value) => applyLinks(linkList.map((item, itemIndex) => (itemIndex === index ? value : item)));
|
|
105
|
+
const deleteLink = (index) => applyLinks(linkList.filter((_, itemIndex) => itemIndex !== index));
|
|
88
106
|
return (_jsx(FileUploadContext, { value: {
|
|
89
107
|
recommendedSize,
|
|
90
108
|
maxSize,
|
|
91
109
|
allowedExtensions,
|
|
92
110
|
fileList,
|
|
111
|
+
linkList,
|
|
93
112
|
maxFileCount,
|
|
94
113
|
disabled,
|
|
95
114
|
readOnly,
|
|
@@ -97,11 +116,15 @@ const Root = ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsPr
|
|
|
97
116
|
addFiles,
|
|
98
117
|
replaceFile,
|
|
99
118
|
deleteFile,
|
|
119
|
+
addLink,
|
|
120
|
+
changeLink,
|
|
121
|
+
deleteLink,
|
|
100
122
|
}, children: _jsx("div", { ref: ref, ...props, children: children }) }));
|
|
101
123
|
};
|
|
102
|
-
const Input = ({ replaceIndex, onClick, onChange, disabled, ref, ...props }) => {
|
|
124
|
+
const Input = ({ replaceIndex, options, onClick, onChange, disabled, ref, ...props }) => {
|
|
103
125
|
const { maxFileCount, locked, addFiles, replaceFile } = useFileUploadContext();
|
|
104
|
-
|
|
126
|
+
const resolvedMaxFileCount = options?.maxFileCount ?? maxFileCount;
|
|
127
|
+
return (_jsx("input", { ref: ref, ...props, type: "file", multiple: replaceIndex === undefined && resolvedMaxFileCount > 1, disabled: locked || disabled, onClick: (event) => {
|
|
105
128
|
onClick?.(event);
|
|
106
129
|
if (!event.defaultPrevented)
|
|
107
130
|
event.currentTarget.value = '';
|
|
@@ -113,9 +136,9 @@ const Input = ({ replaceIndex, onClick, onChange, disabled, ref, ...props }) =>
|
|
|
113
136
|
if (!files.length)
|
|
114
137
|
return;
|
|
115
138
|
if (replaceIndex === undefined)
|
|
116
|
-
await addFiles(files);
|
|
139
|
+
await addFiles(files, options);
|
|
117
140
|
else if (files[0])
|
|
118
|
-
await replaceFile(files[0], replaceIndex);
|
|
141
|
+
await replaceFile(files[0], replaceIndex, options);
|
|
119
142
|
} }));
|
|
120
143
|
};
|
|
121
144
|
export const FileUploadBase = { Root, Input };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type HTMLAttributes } from 'react';
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
2
|
import { type LDButtonProps } from '../../components/LDButton/LDButton.js';
|
|
3
|
-
import { type FileUploadBaseRootProps } from '../../components/LDFileUpload/FileUploadBase.js';
|
|
3
|
+
import { type FileUploadBaseRootProps, type FileUploadOptions } from '../../components/LDFileUpload/FileUploadBase.js';
|
|
4
4
|
export interface LDFileUploadClassNames {
|
|
5
5
|
root?: string;
|
|
6
6
|
input?: string;
|
|
@@ -12,23 +12,36 @@ export interface LDFileUploadRootProps extends FileUploadBaseRootProps {
|
|
|
12
12
|
classNames?: LDFileUploadClassNames;
|
|
13
13
|
}
|
|
14
14
|
export interface LDFileUploadInputProps extends Omit<LDButtonProps<'button'>, 'type'> {
|
|
15
|
-
type?: 'default' | 'image';
|
|
15
|
+
type?: 'default' | 'attachment' | 'image' | 'link';
|
|
16
16
|
label?: string;
|
|
17
|
+
/** 라벨 앞 아이콘입니다. `type="default"`에서는 `leftIcon`을 씁니다. */
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
/** 이 인풋에만 적용할 파일 허용 조건입니다. 생략한 항목은 Root 값을 씁니다. */
|
|
20
|
+
options?: FileUploadOptions;
|
|
17
21
|
}
|
|
18
22
|
export interface LDFileUploadCardListProps {
|
|
19
23
|
type?: 'default' | 'image';
|
|
24
|
+
/** 카드에 업로드 시각 라벨을 표시합니다. `uploadedAt`이 있는 파일만 표시됩니다. */
|
|
25
|
+
isExistUploadedAt?: boolean;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface LDFileUploadLinkListProps {
|
|
29
|
+
placeholder?: string;
|
|
20
30
|
className?: string;
|
|
21
31
|
}
|
|
22
32
|
export type LDFileUploadDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
|
|
23
33
|
export interface LDFileUploadProps extends Omit<LDFileUploadRootProps, 'children' | 'classNames' | 'maxFileCount'> {
|
|
24
34
|
isExistDescription?: boolean;
|
|
35
|
+
/** 카드에 업로드 시각 라벨을 표시합니다. */
|
|
36
|
+
isExistUploadedAt?: boolean;
|
|
25
37
|
}
|
|
26
38
|
/** 한 개의 일반 파일을 업로드하는 기본 조합입니다. */
|
|
27
39
|
declare const LDFileUpload: {
|
|
28
|
-
({ recommendedSize, isExistDescription, fileList, ...props }: LDFileUploadProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
({ recommendedSize, isExistDescription, isExistUploadedAt, fileList, ...props }: LDFileUploadProps): import("react/jsx-runtime").JSX.Element;
|
|
29
41
|
Root: ({ className, classNames, children, ...props }: LDFileUploadRootProps) => import("react/jsx-runtime").JSX.Element;
|
|
30
|
-
Input: ({ type, label, className, ...props }: LDFileUploadInputProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
31
|
-
CardList: ({ type, className, }: LDFileUploadCardListProps) => import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
Input: ({ type, label, icon, options, className, ...props }: LDFileUploadInputProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
43
|
+
CardList: ({ type, isExistUploadedAt, className, }: LDFileUploadCardListProps) => import("react/jsx-runtime").JSX.Element;
|
|
44
|
+
LinkList: ({ placeholder, className, }: LDFileUploadLinkListProps) => import("react/jsx-runtime").JSX.Element;
|
|
32
45
|
Description: ({ className, ...props }: LDFileUploadDescriptionProps) => import("react/jsx-runtime").JSX.Element;
|
|
33
46
|
};
|
|
34
47
|
export default LDFileUpload;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LDFileUpload.d.ts","sourceRoot":"","sources":["../../../src/components/LDFileUpload/LDFileUpload.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LDFileUpload.d.ts","sourceRoot":"","sources":["../../../src/components/LDFileUpload/LDFileUpload.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAEf,OAAiB,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAE9E,OAAuB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EAEvB,MAAM,0CAA0C,CAAC;AAqFlD,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;IACpE,UAAU,CAAC,EAAE,sBAAsB,CAAC;CACrC;AAuBD,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAClD,aAAa,CAAC,QAAQ,CAAC,EACvB,MAAM,CACP;IACC,IAAI,CAAC,EAAE,SAAS,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,CAAC;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,mDAAmD;IACnD,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AA6FD,MAAM,WAAW,yBAAyB;IACxC,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAC3B,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA6HD,MAAM,WAAW,yBAAyB;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAwDD,MAAM,MAAM,4BAA4B,GAAG,cAAc,CAAC,oBAAoB,CAAC,CAAC;AA6BhF,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAC7C,qBAAqB,EACrB,UAAU,GAAG,YAAY,GAAG,cAAc,CAC3C;IACC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,4BAA4B;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,kCAAkC;AAClC,QAAA,MAAM,YAAY;qFAMf,iBAAiB;0DAhWjB,qBAAqB;iEAmCrB,sBAAsB;wDA+FtB,yBAAyB;4CA+HzB,yBAAyB;2CAqDkB,4BAA4B;CAwDzE,CAAC;AAQF,eAAe,YAAY,CAAC"}
|
|
@@ -1,76 +1,172 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import { Images, Link2, Paperclip, Plus, Trash2, X } from 'lucide-react';
|
|
5
|
+
import { createContext, useContext, } from 'react';
|
|
5
6
|
import LDButton from '../../components/LDButton/LDButton.js';
|
|
6
7
|
import LDCarousel from '../../components/LDCarousel/LDCarousel.js';
|
|
7
8
|
import FileUploadBase, { useFileUploadContext, } from '../../components/LDFileUpload/FileUploadBase.js';
|
|
9
|
+
import InputBase from '../../components/LDInput/InputBase.js';
|
|
8
10
|
import RenderIcon from '../../components/RenderIcon/RenderIcon.js';
|
|
11
|
+
import Show from '../../components/Show/Show.js';
|
|
9
12
|
import { FILE_EXTENSIONS } from '../../constants/file.js';
|
|
10
13
|
import cn from '../../utils/cn.js';
|
|
11
|
-
import {
|
|
14
|
+
import { getFileSizeUnit } from '../../utils/file.js';
|
|
15
|
+
import tv from '../../utils/tv.js';
|
|
16
|
+
// disabled·readOnly·기본 상태의 커서는 서로 배타적이다. cursor-not-allowed 는 hover:cursor-pointer 와
|
|
17
|
+
// tailwind-merge 상 다른 그룹이어서 축만으로는 지워지지 않으므로, 겹치지 않게 compound 로 갈라 놓는다.
|
|
18
|
+
const cursorVariants = {
|
|
19
|
+
disabled: { true: 'cursor-not-allowed', false: '' },
|
|
20
|
+
readOnly: { true: '', false: '' },
|
|
21
|
+
};
|
|
22
|
+
const cursorCompounds = [
|
|
23
|
+
{ disabled: false, readOnly: true, className: 'cursor-default' },
|
|
24
|
+
{ disabled: false, readOnly: false, className: 'hover:cursor-pointer' },
|
|
25
|
+
];
|
|
26
|
+
const attachInputVariants = tv({
|
|
27
|
+
base: 'flex w-full flex-row items-center gap-sm rounded-[10px] bg-neutral50 p-5 display16 bold text-neutral700 ld-transition',
|
|
28
|
+
variants: {
|
|
29
|
+
disabled: {
|
|
30
|
+
true: 'cursor-not-allowed opacity-50',
|
|
31
|
+
false: 'hover:cursor-pointer hover:bg-neutral100',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const imageInputVariants = tv({
|
|
36
|
+
base: 'flex size-21 items-center justify-center rounded-xl bg-neutral50 text-neutral500 outline-3 -outline-offset-3 outline-neutral100 outline-dotted ld-transition',
|
|
37
|
+
variants: {
|
|
38
|
+
disabled: {
|
|
39
|
+
true: 'cursor-not-allowed opacity-50',
|
|
40
|
+
false: 'hover:cursor-pointer hover:bg-neutral-100 hover:text-neutral600 hover:outline-neutral200',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const imageCardVariants = tv({
|
|
45
|
+
base: 'group relative block size-21 overflow-hidden rounded-xl outline-3 -outline-offset-3 outline-neutral200',
|
|
46
|
+
variants: cursorVariants,
|
|
47
|
+
compoundVariants: cursorCompounds,
|
|
48
|
+
});
|
|
49
|
+
const imageCardOverlayVariants = tv({
|
|
50
|
+
base: 'pointer-events-none absolute inset-0 opacity-0 ld-transition',
|
|
51
|
+
variants: { locked: { false: 'bg-black/25 group-hover:opacity-100' } },
|
|
52
|
+
});
|
|
53
|
+
const imageCardDeleteVariants = tv({
|
|
54
|
+
base: 'absolute top-2 right-2 flex size-5 min-w-0 items-center justify-center rounded-sm bg-neutral-100 px-0 text-neutral700 opacity-0 ld-transition',
|
|
55
|
+
variants: { locked: { false: 'group-hover:opacity-100' } },
|
|
56
|
+
});
|
|
57
|
+
const cardVariants = tv({
|
|
58
|
+
base: 'flex w-full flex-row items-center justify-between gap-md rounded-[10px] bg-tintBlue p-5',
|
|
59
|
+
variants: cursorVariants,
|
|
60
|
+
compoundVariants: cursorCompounds,
|
|
61
|
+
});
|
|
62
|
+
const cardDeleteVariants = tv({
|
|
63
|
+
base: 'flex size-4.5 min-w-0 items-center justify-center px-0 text-lb500 ld-transition',
|
|
64
|
+
variants: { locked: { true: 'pointer-events-none opacity-0' } },
|
|
65
|
+
});
|
|
66
|
+
// 링크 카드는 파일 카드와 같은 규격(bg-tintBlue·p-5·rounded-[10px])을 쓰되, 안쪽이 실제 input이라
|
|
67
|
+
// 스타일 박스가 래퍼로 올라가고 focus는 내부 input에 가므로 LDInput처럼 focus-within으로 반응한다.
|
|
68
|
+
const linkCardVariants = tv({
|
|
69
|
+
base: 'flex w-full flex-row items-center gap-sm rounded-[10px] bg-tintBlue p-5 outline-2 -outline-offset-2 outline-transparent outline-solid ld-transition focus-within:outline-lb500',
|
|
70
|
+
variants: {
|
|
71
|
+
disabled: { true: 'cursor-not-allowed opacity-50', false: '' },
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const linkFieldVariants = tv({
|
|
75
|
+
base: 'ld-input-field -my-2 h-full w-full min-w-0 border-0 bg-transparent px-0 py-2 display16 bold text-lb500 placeholder-lb300 outline-none read-only:cursor-not-allowed',
|
|
76
|
+
variants: {
|
|
77
|
+
disabled: { true: 'cursor-not-allowed', false: '' },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
12
80
|
const StyleContext = createContext({});
|
|
13
81
|
/** 파일 업로드의 상태와 Styled 파트를 연결하는 루트입니다. */
|
|
14
82
|
const Root = ({ className, classNames = {}, children, ...props }) => (_jsx(StyleContext, { value: classNames, children: _jsx(FileUploadBase.Root, { ...props, className: cn('flex w-full flex-col gap-sm ld-transition', className, classNames.root), children: children }) }));
|
|
15
83
|
/** 새 파일을 선택하는 입력입니다. */
|
|
16
|
-
const Input = ({ type = 'default', label
|
|
17
|
-
const { disabled, readOnly } = useFileUploadContext();
|
|
84
|
+
const Input = ({ type = 'default', label, icon, options, className, ...props }) => {
|
|
85
|
+
const { disabled, readOnly, addLink } = useFileUploadContext();
|
|
18
86
|
const classNames = useContext(StyleContext);
|
|
19
87
|
if (readOnly)
|
|
20
88
|
return null;
|
|
21
|
-
const
|
|
89
|
+
const resolvedLabel = label ??
|
|
90
|
+
{
|
|
91
|
+
attachment: '파일 첨부하기',
|
|
92
|
+
link: '링크 첨부하기',
|
|
93
|
+
default: '파일 선택',
|
|
94
|
+
}[type === 'image' ? 'default' : type];
|
|
95
|
+
const fileInput = (_jsx(FileUploadBase.Input, { options: options, "aria-label": resolvedLabel, className: "sr-only" }));
|
|
96
|
+
// 링크는 파일 선택 대화창이 아니라 빈 입력 행을 추가하는 버튼이라 file input을 두지 않는다.
|
|
97
|
+
if (type === 'link') {
|
|
98
|
+
return (_jsxs("button", { type: "button", onClick: addLink, disabled: disabled, className: attachInputVariants({
|
|
99
|
+
disabled: !!disabled,
|
|
100
|
+
className: cn(className, classNames.input),
|
|
101
|
+
}), children: [_jsx(RenderIcon, { className: "size-5", children: icon ?? _jsx(Link2, {}) }), resolvedLabel] }));
|
|
102
|
+
}
|
|
103
|
+
if (type === 'attachment') {
|
|
104
|
+
return (_jsxs("label", { className: attachInputVariants({
|
|
105
|
+
disabled: !!disabled,
|
|
106
|
+
className: cn(className, classNames.input),
|
|
107
|
+
}), children: [_jsx(RenderIcon, { className: "size-5", children: icon ?? _jsx(Plus, {}) }), resolvedLabel, fileInput] }));
|
|
108
|
+
}
|
|
22
109
|
if (type === 'image') {
|
|
23
|
-
return (_jsxs("label", { className:
|
|
24
|
-
|
|
25
|
-
:
|
|
110
|
+
return (_jsxs("label", { className: imageInputVariants({
|
|
111
|
+
// 미지정도 false 분기(hover)를 타야 하므로 boolean으로 좁혀 넘긴다.
|
|
112
|
+
disabled: !!disabled,
|
|
113
|
+
className: cn(className, classNames.input),
|
|
114
|
+
}), children: [_jsx(RenderIcon, { className: "size-6", children: icon ?? _jsx(Images, {}) }), fileInput] }));
|
|
26
115
|
}
|
|
27
|
-
return (_jsxs(LDButton, { as: "label", ...props, className: cn(className, classNames.input), disabled: disabled, children: [
|
|
116
|
+
return (_jsxs(LDButton, { as: "label", ...props, className: cn(className, classNames.input), disabled: disabled, children: [resolvedLabel, fileInput] }));
|
|
28
117
|
};
|
|
29
118
|
/** 선택된 파일을 교체하거나 삭제할 수 있는 목록입니다. */
|
|
30
|
-
const CardList = ({ type = 'default', className, }) => {
|
|
119
|
+
const CardList = ({ type = 'default', isExistUploadedAt = true, className, }) => {
|
|
31
120
|
const { fileList, disabled, readOnly, locked, deleteFile } = useFileUploadContext();
|
|
32
121
|
const classNames = useContext(StyleContext);
|
|
33
122
|
if (type === 'image') {
|
|
34
|
-
const cards = fileList.map((file, index) => (_jsxs("label", { className:
|
|
35
|
-
|
|
36
|
-
: readOnly
|
|
37
|
-
|
|
38
|
-
|
|
123
|
+
const cards = fileList.map((file, index) => (_jsxs("label", { className: imageCardVariants({
|
|
124
|
+
disabled: !!disabled,
|
|
125
|
+
readOnly: !!readOnly,
|
|
126
|
+
className: classNames.card,
|
|
127
|
+
}), children: [_jsx("img", { src: file.src, alt: file.fileName, className: "size-full object-cover" }), _jsx("span", { className: imageCardOverlayVariants({ locked }) }), _jsx(FileUploadBase.Input, { replaceIndex: index, "aria-label": `${file.fileName} 교체`, className: "sr-only" }), _jsx(LDButton, { onClick: async (event) => {
|
|
39
128
|
event.preventDefault();
|
|
40
129
|
event.stopPropagation();
|
|
41
130
|
await deleteFile(index);
|
|
42
|
-
}, disabled: locked, variant: "ghost", leftIcon: _jsx(Trash2, {}), color: "neutral", className:
|
|
131
|
+
}, disabled: locked, variant: "ghost", leftIcon: _jsx(Trash2, {}), color: "neutral", className: imageCardDeleteVariants({ locked }) })] }, `${file.fileName}_${index}`)));
|
|
43
132
|
if (fileList.length <= 1) {
|
|
44
133
|
return (_jsx("div", { className: cn('flex flex-row gap-sm', className, classNames.cardList), children: cards }));
|
|
45
134
|
}
|
|
46
135
|
return (_jsx(LDCarousel, { className: cn(className, classNames.cardList), slidesPerView: "auto", gap: 8, arrowPosition: "none", showDots: false, slides: cards }));
|
|
47
136
|
}
|
|
48
|
-
return (_jsx("div", { className: cn('flex w-full flex-col gap-sm', className, classNames.cardList), children: fileList.map((file, index) => (_jsxs("label", { className:
|
|
49
|
-
|
|
50
|
-
: readOnly
|
|
51
|
-
|
|
52
|
-
|
|
137
|
+
return (_jsx("div", { className: cn('flex w-full flex-col gap-sm', className, classNames.cardList), children: fileList.map((file, index) => (_jsxs("label", { className: cardVariants({
|
|
138
|
+
disabled: !!disabled,
|
|
139
|
+
readOnly: !!readOnly,
|
|
140
|
+
className: classNames.card,
|
|
141
|
+
}), children: [_jsxs("div", { className: "flex flex-row items-center gap-sm", children: [_jsx(RenderIcon, { className: "size-5 text-lb500", children: _jsx(Paperclip, {}) }), _jsxs("div", { className: "flex items-center justify-center gap-sm mobile:flex-col mobile:items-start mobile:gap-2xs", children: [_jsx("span", { className: "display16 bold text-lb500", children: file.fileName }), _jsx(Show, { when: isExistUploadedAt && file.uploadedAt, children: (uploadedAt) => (_jsxs("span", { className: "rounded-[5px] bg-blue200 px-1.5 py-0.75 display10 medium text-white", children: [dayjs(uploadedAt).format('YYYY-MM-DD HH:mm'), " \uB9C8\uC9C0\uB9C9 \uC5C5\uB85C\uB4DC"] })) })] })] }), _jsx(FileUploadBase.Input, { replaceIndex: index, "aria-label": `${file.fileName} 교체`, className: "sr-only" }), _jsx(LDButton, { onClick: async (event) => {
|
|
53
142
|
event.preventDefault();
|
|
54
143
|
event.stopPropagation();
|
|
55
144
|
await deleteFile(index);
|
|
56
|
-
}, disabled: locked, variant: "ghost", leftIcon: _jsx(
|
|
145
|
+
}, disabled: locked, variant: "ghost", leftIcon: _jsx(X, { className: "size-4.5" }), color: "neutral", className: cardDeleteVariants({ locked }) })] }, `${file.fileName}_${index}`))) }));
|
|
146
|
+
};
|
|
147
|
+
/** 첨부된 링크를 실시간으로 편집하거나 삭제할 수 있는 목록입니다. */
|
|
148
|
+
const LinkList = ({ placeholder = '링크를 입력해주세요', className, }) => {
|
|
149
|
+
const { linkList, disabled, readOnly, locked, changeLink, deleteLink } = useFileUploadContext();
|
|
150
|
+
const classNames = useContext(StyleContext);
|
|
151
|
+
return (_jsx("div", { className: cn('flex w-full flex-col gap-sm', className, classNames.cardList), children: linkList.map((link, index) => (_jsxs(InputBase.Root, { block: true, className: linkCardVariants({
|
|
152
|
+
disabled: !!disabled,
|
|
153
|
+
className: classNames.card,
|
|
154
|
+
}), children: [_jsx(RenderIcon, { className: "size-5 shrink-0 text-lb500", children: _jsx(Link2, {}) }), _jsx(InputBase.Field, { type: "url", value: link, placeholder: placeholder, "aria-label": `링크 ${index + 1}`, disabled: disabled, readOnly: readOnly, onChange: (event) => changeLink(index, event.target.value), className: linkFieldVariants({ disabled: !!disabled }) }), _jsx(LDButton, { onClick: () => deleteLink(index), "aria-label": `링크 ${index + 1} 삭제`, disabled: locked, variant: "ghost", leftIcon: _jsx(X, { className: "size-4.5" }), color: "neutral", className: cardDeleteVariants({ locked }) })] }, index))) }));
|
|
57
155
|
};
|
|
58
156
|
/** 업로드 제한 조건을 안내합니다. */
|
|
59
157
|
const Description = ({ className, ...props }) => {
|
|
60
158
|
const { recommendedSize, maxSize, allowedExtensions, maxFileCount } = useFileUploadContext();
|
|
61
159
|
const classNames = useContext(StyleContext);
|
|
62
|
-
const recommendedSizeText = recommendedSize
|
|
63
|
-
? `권장 사이즈 ${recommendedSize.width}x${recommendedSize.height}, `
|
|
64
|
-
: '';
|
|
65
160
|
const extensionsText = allowedExtensions
|
|
66
161
|
.map((item) => FILE_EXTENSIONS[item])
|
|
67
162
|
.join(', ');
|
|
68
|
-
return (_jsxs("p", { ...props, className: cn('display12 semibold text-neutral400', className, classNames.description), children: [
|
|
163
|
+
return (_jsxs("p", { ...props, className: cn('display12 semibold text-neutral400', className, classNames.description), children: [_jsx(Show, { when: recommendedSize, children: ({ width, height }) => `권장 사이즈 ${width}x${height}, ` }), "\uCD5C\uB300 \uD06C\uAE30 ", getFileSizeUnit(maxSize), " (", extensionsText, ")", _jsx(Show, { when: maxFileCount > 1, children: `, 최대 ${maxFileCount}개` })] }));
|
|
69
164
|
};
|
|
70
165
|
/** 한 개의 일반 파일을 업로드하는 기본 조합입니다. */
|
|
71
|
-
const LDFileUpload = ({ recommendedSize = { width: 1200, height: 800 }, isExistDescription, fileList, ...props }) => (_jsxs(Root, { ...props, fileList: fileList, recommendedSize: recommendedSize, maxFileCount: 1, children: [fileList[0]?.src
|
|
166
|
+
const LDFileUpload = ({ recommendedSize = { width: 1200, height: 800 }, isExistDescription, isExistUploadedAt, fileList, ...props }) => (_jsxs(Root, { ...props, fileList: fileList, recommendedSize: recommendedSize, maxFileCount: 1, children: [_jsx(Show, { when: fileList[0]?.src, fallback: _jsx(Input, { className: "w-fit" }), children: _jsx(CardList, { isExistUploadedAt: isExistUploadedAt }) }), _jsx(Show, { when: isExistDescription, children: _jsx(Description, {}) })] }));
|
|
72
167
|
LDFileUpload.Root = Root;
|
|
73
168
|
LDFileUpload.Input = Input;
|
|
74
169
|
LDFileUpload.CardList = CardList;
|
|
170
|
+
LDFileUpload.LinkList = LinkList;
|
|
75
171
|
LDFileUpload.Description = Description;
|
|
76
172
|
export default LDFileUpload;
|