@nexus-cross/design-system 1.0.10 → 1.0.11

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.
@@ -1097,14 +1097,14 @@ Empty state placeholder. Shown when data is empty or unavailable.
1097
1097
 
1098
1098
  ## Breadcrumb
1099
1099
 
1100
- Breadcrumb navigation. Shows current location path.
1100
+ Breadcrumb navigation (compound component pattern). Use <Breadcrumb.Item> children instead of items array. Each Item can wrap arbitrary ReactNode (Link, Select, plain text, etc.).
1101
1101
 
1102
1102
  | Prop | Type | Default | Description |
1103
1103
  |---|---|---|---|
1104
- | `items` | `object`[] | - | Breadcrumb items array (required) |
1105
- | `separator` | `ReactNode` | - | Custom separator (ReactNode). Default: chevron |
1106
- | `maxItems` | `number` | - | Max visible items (collapses middle with "...") |
1107
- | `className` | `string` | - | Style override |
1104
+ | `children` | `ReactNode` | - | One or more <Breadcrumb.Item> elements |
1105
+ | `separator` | `ReactNode` | - | Custom separator (ReactNode). Default: chevron icon |
1106
+ | `maxItems` | `number` | - | Max visible items. When exceeded, middle items collapse with "" |
1107
+ | `className` | `string` | - | Style override for the root nav element |
1108
1108
 
1109
1109
  ---
1110
1110
 
@@ -1235,6 +1235,28 @@ DatePicker. Calendar popup for date selection. Based on react-day-picker.
1235
1235
 
1236
1236
  ---
1237
1237
 
1238
+ ## ImageUpload
1239
+
1240
+ ImageUpload. Drag-and-drop image upload with preview, file validation, and field label/description support.
1241
+
1242
+ | Prop | Type | Default | Description |
1243
+ |---|---|---|---|
1244
+ | `value` | `string` | - | Controlled image URL (string | null) |
1245
+ | `defaultValue` | `string` | - | Default image URL |
1246
+ | `onChange` | `ReactNode` | - | File change callback (file: File | null) => void |
1247
+ | `onError` | `ReactNode` | - | Validation error callback (error: string) => void |
1248
+ | `accept` | `string`[] | `["jpg","jpeg","png","gif","webp"]` | Allowed file extensions |
1249
+ | `maxSize` | `number` | `2097152` | Max file size in bytes (default 2MB) |
1250
+ | `placeholder` | `string` | `"이미지를 드래그하거나 파일 선택"` | Empty state placeholder text |
1251
+ | `formatDescription` | `string` | - | Format description override (default: auto-generated from accept/maxSize) |
1252
+ | `label` | `ReactNode` | - | Field label (ReactNode) |
1253
+ | `description` | `ReactNode` | - | Field description below the upload area (ReactNode) |
1254
+ | `disabled` | `boolean` | `false` | Disabled state |
1255
+ | `size` | `'sm'` \| `'md'` \| `'lg'` | `"md"` | Upload area size |
1256
+ | `className` | `string` | - | Style override |
1257
+
1258
+ ---
1259
+
1238
1260
  ## Hooks
1239
1261
 
1240
1262
  ### useModal
@@ -0,0 +1,279 @@
1
+ 'use strict';
2
+
3
+ var chunkCZC76ZD5_js = require('./chunk-CZC76ZD5.js');
4
+ var React = require('react');
5
+ var classVarianceAuthority = require('class-variance-authority');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
27
+
28
+ var imageUploadVariants = classVarianceAuthority.cva("nexus-image-upload", {
29
+ variants: {
30
+ size: {
31
+ sm: "nexus-image-upload--sm",
32
+ md: "nexus-image-upload--md",
33
+ lg: "nexus-image-upload--lg"
34
+ }
35
+ },
36
+ defaultVariants: { size: "md" }
37
+ });
38
+ var DEFAULT_ACCEPT = ["jpg", "jpeg", "png", "gif", "webp"];
39
+ var DEFAULT_MAX_SIZE = 2 * 1024 * 1024;
40
+ var ImageUpIcon = ({ className }) => /* @__PURE__ */ jsxRuntime.jsxs("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
41
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21" }),
42
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m13.5 19 3-3 3 3" }),
43
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16.5 22v-6" }),
44
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "9", r: "2" })
45
+ ] });
46
+ var CloseIcon = ({ className }) => /* @__PURE__ */ jsxRuntime.jsx("svg", { className, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 4L4 12M4 4l8 8" }) });
47
+ function formatBytes(bytes) {
48
+ if (bytes >= 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))}MB`;
49
+ return `${Math.round(bytes / 1024)}KB`;
50
+ }
51
+ var ImageUpload = React__namespace.forwardRef(
52
+ ({
53
+ value: controlledValue,
54
+ defaultValue,
55
+ onChange,
56
+ onError,
57
+ accept = DEFAULT_ACCEPT,
58
+ maxSize = DEFAULT_MAX_SIZE,
59
+ placeholder = "\uC774\uBBF8\uC9C0\uB97C \uB4DC\uB798\uADF8\uD558\uAC70\uB098 \uD30C\uC77C \uC120\uD0DD",
60
+ formatDescription: formatDescProp,
61
+ label,
62
+ description,
63
+ disabled = false,
64
+ size,
65
+ className
66
+ }, ref) => {
67
+ const isControlled = controlledValue !== void 0;
68
+ const [internalPreview, setInternalPreview] = React__namespace.useState(
69
+ defaultValue ?? null
70
+ );
71
+ const preview = isControlled ? controlledValue : internalPreview;
72
+ const [isDragging, setIsDragging] = React__namespace.useState(false);
73
+ const inputRef = React__namespace.useRef(null);
74
+ const dragCounter = React__namespace.useRef(0);
75
+ const acceptMime = React__namespace.useMemo(
76
+ () => accept.map((ext) => {
77
+ const lower = ext.toLowerCase().replace(/^\./, "");
78
+ if (lower === "jpg" || lower === "jpeg") return "image/jpeg";
79
+ return `image/${lower}`;
80
+ }),
81
+ [accept]
82
+ );
83
+ const formatText = formatDescProp ?? `${[...new Set(accept.map((e) => e.toUpperCase().replace(/^\./, "")))].join(" \xB7 ")} \xB7 \uCD5C\uB300 ${formatBytes(maxSize)}`;
84
+ const validateFile = React__namespace.useCallback(
85
+ (file) => {
86
+ if (!acceptMime.includes(file.type)) {
87
+ return `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4. (${accept.join(", ")})`;
88
+ }
89
+ if (file.size > maxSize) {
90
+ return `\uD30C\uC77C \uD06C\uAE30\uAC00 ${formatBytes(maxSize)}\uB97C \uCD08\uACFC\uD569\uB2C8\uB2E4.`;
91
+ }
92
+ return null;
93
+ },
94
+ [acceptMime, accept, maxSize]
95
+ );
96
+ const handleFile = React__namespace.useCallback(
97
+ (file) => {
98
+ const error = validateFile(file);
99
+ if (error) {
100
+ onError?.(error);
101
+ return;
102
+ }
103
+ if (!isControlled) {
104
+ const url = URL.createObjectURL(file);
105
+ setInternalPreview((prev) => {
106
+ if (prev?.startsWith("blob:")) URL.revokeObjectURL(prev);
107
+ return url;
108
+ });
109
+ }
110
+ onChange?.(file);
111
+ },
112
+ [validateFile, isControlled, onChange, onError]
113
+ );
114
+ const handleRemove = React__namespace.useCallback(() => {
115
+ if (!isControlled) {
116
+ setInternalPreview((prev) => {
117
+ if (prev?.startsWith("blob:")) URL.revokeObjectURL(prev);
118
+ return null;
119
+ });
120
+ }
121
+ onChange?.(null);
122
+ if (inputRef.current) inputRef.current.value = "";
123
+ }, [isControlled, onChange]);
124
+ const handleInputChange = React__namespace.useCallback(
125
+ (e) => {
126
+ const file = e.target.files?.[0];
127
+ if (file) handleFile(file);
128
+ },
129
+ [handleFile]
130
+ );
131
+ const handleClick = React__namespace.useCallback(() => {
132
+ if (!disabled) inputRef.current?.click();
133
+ }, [disabled]);
134
+ const handleDragEnter = React__namespace.useCallback(
135
+ (e) => {
136
+ e.preventDefault();
137
+ e.stopPropagation();
138
+ if (disabled) return;
139
+ dragCounter.current += 1;
140
+ if (dragCounter.current === 1) setIsDragging(true);
141
+ },
142
+ [disabled]
143
+ );
144
+ const handleDragLeave = React__namespace.useCallback((e) => {
145
+ e.preventDefault();
146
+ e.stopPropagation();
147
+ dragCounter.current -= 1;
148
+ if (dragCounter.current === 0) setIsDragging(false);
149
+ }, []);
150
+ const handleDragOver = React__namespace.useCallback((e) => {
151
+ e.preventDefault();
152
+ e.stopPropagation();
153
+ }, []);
154
+ const handleDrop = React__namespace.useCallback(
155
+ (e) => {
156
+ e.preventDefault();
157
+ e.stopPropagation();
158
+ setIsDragging(false);
159
+ dragCounter.current = 0;
160
+ if (disabled) return;
161
+ const file = e.dataTransfer.files?.[0];
162
+ if (file) handleFile(file);
163
+ },
164
+ [disabled, handleFile]
165
+ );
166
+ React__namespace.useEffect(() => {
167
+ return () => {
168
+ if (!isControlled && internalPreview?.startsWith("blob:")) {
169
+ URL.revokeObjectURL(internalPreview);
170
+ }
171
+ };
172
+ }, []);
173
+ const hasField = !!(label || description);
174
+ const hiddenInput = /* @__PURE__ */ jsxRuntime.jsx(
175
+ "input",
176
+ {
177
+ ref: inputRef,
178
+ type: "file",
179
+ accept: acceptMime.join(","),
180
+ onChange: handleInputChange,
181
+ className: "nexus-image-upload__input",
182
+ tabIndex: -1
183
+ }
184
+ );
185
+ const previewView = /* @__PURE__ */ jsxRuntime.jsxs(
186
+ "div",
187
+ {
188
+ className: chunkCZC76ZD5_js.cn(
189
+ "nexus-image-upload--has-preview",
190
+ size === "sm" && "nexus-image-upload--sm",
191
+ size === "lg" && "nexus-image-upload--lg",
192
+ disabled && "nexus-image-upload--disabled",
193
+ !hasField && className
194
+ ),
195
+ children: [
196
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nexus-image-upload__preview-wrapper", children: [
197
+ /* @__PURE__ */ jsxRuntime.jsx(
198
+ "img",
199
+ {
200
+ src: preview,
201
+ alt: "Uploaded preview",
202
+ className: "nexus-image-upload__preview"
203
+ }
204
+ ),
205
+ !disabled && /* @__PURE__ */ jsxRuntime.jsx(
206
+ "button",
207
+ {
208
+ type: "button",
209
+ className: "nexus-image-upload__remove",
210
+ onClick: handleRemove,
211
+ "aria-label": "\uC774\uBBF8\uC9C0 \uC0AD\uC81C",
212
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { className: "nexus-image-upload__remove-icon" })
213
+ }
214
+ )
215
+ ] }),
216
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nexus-image-upload__info", children: [
217
+ !disabled && /* @__PURE__ */ jsxRuntime.jsx(
218
+ "button",
219
+ {
220
+ type: "button",
221
+ className: "nexus-image-upload__change-btn",
222
+ onClick: handleClick,
223
+ children: "\uC774\uBBF8\uC9C0 \uBCC0\uACBD"
224
+ }
225
+ ),
226
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nexus-image-upload__format", children: formatText })
227
+ ] }),
228
+ hiddenInput
229
+ ]
230
+ }
231
+ );
232
+ const emptyView = /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nexus-image-upload__container", children: /* @__PURE__ */ jsxRuntime.jsxs(
233
+ "div",
234
+ {
235
+ role: "button",
236
+ tabIndex: disabled ? -1 : 0,
237
+ className: chunkCZC76ZD5_js.cn(
238
+ imageUploadVariants({ size }),
239
+ isDragging && "nexus-image-upload--dragging",
240
+ disabled && "nexus-image-upload--disabled",
241
+ !hasField && className
242
+ ),
243
+ onClick: handleClick,
244
+ onKeyDown: (e) => {
245
+ if (e.key === "Enter" || e.key === " ") {
246
+ e.preventDefault();
247
+ handleClick();
248
+ }
249
+ },
250
+ onDragEnter: handleDragEnter,
251
+ onDragLeave: handleDragLeave,
252
+ onDragOver: handleDragOver,
253
+ onDrop: handleDrop,
254
+ "aria-label": placeholder,
255
+ children: [
256
+ /* @__PURE__ */ jsxRuntime.jsx(ImageUpIcon, { className: "nexus-image-upload__icon" }),
257
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nexus-image-upload__text-group", children: [
258
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nexus-image-upload__text", children: placeholder }),
259
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nexus-image-upload__format", children: formatText })
260
+ ] }),
261
+ hiddenInput
262
+ ]
263
+ }
264
+ ) });
265
+ const uploadBox = preview ? previewView : emptyView;
266
+ if (!hasField) {
267
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className, children: uploadBox });
268
+ }
269
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: chunkCZC76ZD5_js.cn("nexus-image-upload-field", className), children: [
270
+ label && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nexus-image-upload-field__label", children: label }),
271
+ uploadBox,
272
+ description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "nexus-image-upload-field__description", children: description })
273
+ ] });
274
+ }
275
+ );
276
+ ImageUpload.displayName = "ImageUpload";
277
+
278
+ exports.ImageUpload = ImageUpload;
279
+ exports.imageUploadVariants = imageUploadVariants;