@poodle-kit/ui 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,46 +1,987 @@
1
- // src/components/Button.tsx
2
- import { jsx } from "react/jsx-runtime";
3
- var base = "inline-flex items-center justify-center rounded-lg font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
4
- var sizes = {
5
- sm: "h-8 px-3 text-sm",
6
- md: "h-10 px-4 text-sm",
7
- lg: "h-12 px-5 text-base"
8
- };
9
- var variants = {
10
- primary: "bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-600 shadow-sm",
11
- secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-400 border border-gray-200",
12
- ghost: "bg-transparent text-gray-900 hover:bg-gray-100 focus-visible:ring-gray-400",
13
- danger: "bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-600 shadow-sm"
14
- };
15
- function cx(...classes) {
16
- return classes.filter(Boolean).join(" ");
1
+ // src/components/button/button.tsx
2
+ import { forwardRef } from "react";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { cva } from "class-variance-authority";
5
+
6
+ // src/lib/cn.ts
7
+ import { clsx } from "clsx";
8
+ import { twMerge } from "tailwind-merge";
9
+ function cn(...inputs) {
10
+ return twMerge(clsx(inputs));
17
11
  }
18
- function Button({
19
- label,
12
+
13
+ // src/components/button/button.tsx
14
+ import { jsx } from "react/jsx-runtime";
15
+ var buttonVariants = cva(
16
+ "inline-flex items-center justify-center rounded-[var(--radius-md)] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
17
+ {
18
+ variants: {
19
+ variant: {
20
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
21
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
22
+ info: "bg-info text-info-foreground hover:bg-info/90",
23
+ danger: "bg-danger text-danger-foreground hover:bg-danger/90",
24
+ warning: "bg-warning text-warning-foreground hover:bg-warning/90",
25
+ success: "bg-success text-success-foreground hover:bg-success/90",
26
+ ghost: "hover:bg-accent hover:text-accent-foreground",
27
+ outline: "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
28
+ link: "text-primary underline-offset-4 hover:underline"
29
+ },
30
+ size: {
31
+ default: "h-10 px-4 py-2",
32
+ sm: "h-9 rounded-[var(--radius-md)] px-3",
33
+ lg: "h-11 rounded-[var(--radius-md)] px-8",
34
+ icon: "h-10 w-10"
35
+ }
36
+ },
37
+ defaultVariants: {
38
+ variant: "default",
39
+ size: "default"
40
+ }
41
+ }
42
+ );
43
+ var Button = forwardRef(
44
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
45
+ const Comp = asChild ? Slot : "button";
46
+ return /* @__PURE__ */ jsx(
47
+ Comp,
48
+ {
49
+ className: cn(buttonVariants({ variant, size, className })),
50
+ ref,
51
+ type: "button",
52
+ ...props
53
+ }
54
+ );
55
+ }
56
+ );
57
+ Button.displayName = "Button";
58
+
59
+ // src/components/input/input.tsx
60
+ import {
61
+ forwardRef as forwardRef2
62
+ } from "react";
63
+ import { cva as cva2 } from "class-variance-authority";
64
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
65
+ var inputVariants = cva2(
66
+ "flex w-full rounded-[var(--radius-md)] border border-input bg-background text-sm placeholder:text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
67
+ {
68
+ variants: {
69
+ size: {
70
+ default: "h-10 px-3 py-2",
71
+ sm: "h-9 px-2 text-xs",
72
+ lg: "h-11 px-4"
73
+ },
74
+ state: {
75
+ default: "focus-visible:ring-ring",
76
+ error: "border-danger focus-visible:ring-danger",
77
+ success: "border-success focus-visible:ring-success"
78
+ }
79
+ },
80
+ defaultVariants: {
81
+ size: "default",
82
+ state: "default"
83
+ }
84
+ }
85
+ );
86
+ var Input = forwardRef2(
87
+ ({
88
+ className,
89
+ inputClassName,
90
+ size,
91
+ error,
92
+ success,
93
+ label,
94
+ id,
95
+ leftIcon,
96
+ rightIcon,
97
+ onLeftIconClick,
98
+ onRightIconClick,
99
+ required,
100
+ disabled,
101
+ ...props
102
+ }, ref) => {
103
+ const state = error ? "error" : success ? "success" : "default";
104
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-1", className), children: [
105
+ label && /* @__PURE__ */ jsxs(
106
+ "label",
107
+ {
108
+ htmlFor: id,
109
+ className: cn(
110
+ "text-sm font-medium",
111
+ disabled && "opacity-50"
112
+ ),
113
+ children: [
114
+ label,
115
+ required && /* @__PURE__ */ jsx2("span", { className: "ml-1 text-danger", "aria-label": "\uD544\uC218", children: "*" })
116
+ ]
117
+ }
118
+ ),
119
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
120
+ leftIcon && /* @__PURE__ */ jsx2(
121
+ "button",
122
+ {
123
+ type: "button",
124
+ disabled: !onLeftIconClick,
125
+ "aria-label": "left icon",
126
+ className: cn(
127
+ "absolute left-3 top-1/2 z-10 -translate-y-1/2",
128
+ onLeftIconClick ? "cursor-pointer" : "pointer-events-none cursor-default",
129
+ disabled && "opacity-50"
130
+ ),
131
+ onClick: onLeftIconClick,
132
+ children: leftIcon
133
+ }
134
+ ),
135
+ /* @__PURE__ */ jsx2(
136
+ "input",
137
+ {
138
+ id,
139
+ ref,
140
+ required,
141
+ disabled,
142
+ "aria-invalid": error || void 0,
143
+ className: cn(
144
+ inputVariants({ size, state }),
145
+ // 아이콘이 있을 때 텍스트가 아이콘에 가리지 않도록 패딩 추가
146
+ leftIcon && "pl-9",
147
+ rightIcon && "pr-9",
148
+ inputClassName
149
+ ),
150
+ ...props
151
+ }
152
+ ),
153
+ rightIcon && /* @__PURE__ */ jsx2(
154
+ "button",
155
+ {
156
+ type: "button",
157
+ disabled: !onRightIconClick,
158
+ "aria-label": "right icon",
159
+ className: cn(
160
+ "absolute right-3 top-1/2 z-10 -translate-y-1/2",
161
+ onRightIconClick ? "cursor-pointer" : "pointer-events-none cursor-default",
162
+ disabled && "opacity-50"
163
+ ),
164
+ onClick: onRightIconClick,
165
+ children: rightIcon
166
+ }
167
+ )
168
+ ] })
169
+ ] });
170
+ }
171
+ );
172
+ Input.displayName = "Input";
173
+
174
+ // src/components/input/input-message.tsx
175
+ import { jsx as jsx3 } from "react/jsx-runtime";
176
+ function InputMessage({
20
177
  children,
21
- variant = "primary",
22
- size = "md",
23
- fullWidth = false,
24
- className,
25
- ...rest
178
+ error,
179
+ className
26
180
  }) {
27
- return /* @__PURE__ */ jsx(
181
+ return /* @__PURE__ */ jsx3(
182
+ "p",
183
+ {
184
+ className: cn(
185
+ "text-xs",
186
+ error ? "text-danger" : "text-muted-foreground",
187
+ className
188
+ ),
189
+ children
190
+ }
191
+ );
192
+ }
193
+
194
+ // src/components/image-uploader/image-uploader.tsx
195
+ import { forwardRef as forwardRef3, useId } from "react";
196
+ import { cva as cva3 } from "class-variance-authority";
197
+
198
+ // src/icons/index.tsx
199
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
200
+ function createIcon(paths, defaultSize = 24) {
201
+ return function Icon({
202
+ width = defaultSize,
203
+ height = defaultSize,
204
+ ...props
205
+ }) {
206
+ return /* @__PURE__ */ jsx4(
207
+ "svg",
208
+ {
209
+ xmlns: "http://www.w3.org/2000/svg",
210
+ width,
211
+ height,
212
+ viewBox: "0 0 24 24",
213
+ fill: "none",
214
+ stroke: "currentColor",
215
+ strokeWidth: "2",
216
+ strokeLinecap: "round",
217
+ strokeLinejoin: "round",
218
+ "aria-hidden": "true",
219
+ ...props,
220
+ children: paths
221
+ }
222
+ );
223
+ };
224
+ }
225
+ var PlusIcon = createIcon(
226
+ /* @__PURE__ */ jsxs2(Fragment, { children: [
227
+ /* @__PURE__ */ jsx4("path", { d: "M5 12h14" }),
228
+ /* @__PURE__ */ jsx4("path", { d: "M12 5v14" })
229
+ ] })
230
+ );
231
+ var XIcon = createIcon(
232
+ /* @__PURE__ */ jsxs2(Fragment, { children: [
233
+ /* @__PURE__ */ jsx4("path", { d: "M18 6 6 18" }),
234
+ /* @__PURE__ */ jsx4("path", { d: "m6 6 12 12" })
235
+ ] }),
236
+ 12
237
+ );
238
+
239
+ // src/components/image-uploader/use-image-uploader.ts
240
+ import {
241
+ useCallback,
242
+ useEffect,
243
+ useMemo,
244
+ useRef,
245
+ useState
246
+ } from "react";
247
+ function matchesAccept(file, accept) {
248
+ return accept.split(",").map((s) => s.trim().toLowerCase()).some((pattern) => {
249
+ if (pattern === "*" || pattern === "*/*") return true;
250
+ if (pattern.startsWith("."))
251
+ return file.name.toLowerCase().endsWith(pattern);
252
+ if (pattern.endsWith("/*"))
253
+ return file.type.startsWith(pattern.slice(0, -2));
254
+ return file.type === pattern;
255
+ });
256
+ }
257
+ function useImageUploader({
258
+ initialImages,
259
+ maxImages = 4,
260
+ maxSizeMb,
261
+ accept = "image/*",
262
+ onFileSelect,
263
+ onDeleteExisting,
264
+ onError
265
+ }) {
266
+ const [newImages, setNewImages] = useState([]);
267
+ const [deletedIds, setDeletedIds] = useState([]);
268
+ const [isDragActive, setIsDragActive] = useState(false);
269
+ const inputRef = useRef(null);
270
+ const onFileSelectRef = useRef(onFileSelect);
271
+ const onDeleteExistingRef = useRef(onDeleteExisting);
272
+ const onErrorRef = useRef(onError);
273
+ useEffect(() => {
274
+ onFileSelectRef.current = onFileSelect;
275
+ onDeleteExistingRef.current = onDeleteExisting;
276
+ onErrorRef.current = onError;
277
+ });
278
+ const newImagesRef = useRef([]);
279
+ const deletedIdsRef = useRef([]);
280
+ const existingCountRef = useRef(0);
281
+ const existingImages = useMemo(
282
+ () => (initialImages != null ? initialImages : []).filter(
283
+ (img) => !deletedIds.includes(img.id)
284
+ ),
285
+ [initialImages, deletedIds]
286
+ );
287
+ useEffect(() => {
288
+ newImagesRef.current = newImages;
289
+ }, [newImages]);
290
+ useEffect(() => {
291
+ deletedIdsRef.current = deletedIds;
292
+ }, [deletedIds]);
293
+ useEffect(() => {
294
+ existingCountRef.current = existingImages.length;
295
+ }, [existingImages.length]);
296
+ useEffect(() => {
297
+ return () => {
298
+ newImagesRef.current.forEach(
299
+ (item) => URL.revokeObjectURL(item.preview)
300
+ );
301
+ };
302
+ }, []);
303
+ const processFiles = useCallback(
304
+ (files) => {
305
+ const currentTotal = existingCountRef.current + newImagesRef.current.length;
306
+ const onErr = onErrorRef.current;
307
+ const onSelect = onFileSelectRef.current;
308
+ if (currentTotal >= maxImages) {
309
+ onErr == null ? void 0 : onErr({
310
+ type: "MAX_IMAGES",
311
+ message: `\uC774\uBBF8\uC9C0\uB294 \uCD5C\uB300 ${maxImages}\uC7A5\uAE4C\uC9C0 \uCD94\uAC00\uD560 \uC218 \uC788\uC5B4\uC694.`,
312
+ maxImages
313
+ });
314
+ return;
315
+ }
316
+ const remainingSlots = maxImages - currentTotal;
317
+ if (files.length > remainingSlots) {
318
+ onErr == null ? void 0 : onErr({
319
+ type: "MAX_IMAGES",
320
+ message: `\uC774\uBBF8\uC9C0\uB294 \uCD5C\uB300 ${maxImages}\uC7A5\uAE4C\uC9C0 \uCD94\uAC00\uD560 \uC218 \uC788\uC5B4\uC694.`,
321
+ maxImages
322
+ });
323
+ }
324
+ const validFiles = [];
325
+ for (const file of files.slice(0, remainingSlots)) {
326
+ if (!matchesAccept(file, accept)) {
327
+ onErr == null ? void 0 : onErr({
328
+ type: "INVALID_TYPE",
329
+ message: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC774\uC5D0\uC694.`,
330
+ file,
331
+ accept
332
+ });
333
+ continue;
334
+ }
335
+ if (maxSizeMb !== void 0 && file.size > maxSizeMb * 1024 * 1024) {
336
+ onErr == null ? void 0 : onErr({
337
+ type: "MAX_SIZE",
338
+ message: `${file.name}\uC758 \uD06C\uAE30\uAC00 ${maxSizeMb}MB\uB97C \uCD08\uACFC\uD574\uC694.`,
339
+ maxSizeMb,
340
+ file
341
+ });
342
+ continue;
343
+ }
344
+ validFiles.push({
345
+ file,
346
+ preview: URL.createObjectURL(file),
347
+ id: crypto.randomUUID()
348
+ });
349
+ }
350
+ if (validFiles.length === 0) return;
351
+ const next = [...newImagesRef.current, ...validFiles];
352
+ newImagesRef.current = next;
353
+ setNewImages(next);
354
+ onSelect == null ? void 0 : onSelect(next.map((item) => item.file));
355
+ },
356
+ [maxImages, accept, maxSizeMb]
357
+ );
358
+ const removeExisting = useCallback((id) => {
359
+ var _a;
360
+ const next = [...deletedIdsRef.current, id];
361
+ deletedIdsRef.current = next;
362
+ setDeletedIds(next);
363
+ (_a = onDeleteExistingRef.current) == null ? void 0 : _a.call(onDeleteExistingRef, next);
364
+ }, []);
365
+ const removeNew = useCallback((id) => {
366
+ var _a;
367
+ const current = newImagesRef.current;
368
+ const toRemove = current.find((item) => item.id === id);
369
+ if (toRemove) URL.revokeObjectURL(toRemove.preview);
370
+ const next = current.filter((item) => item.id !== id);
371
+ newImagesRef.current = next;
372
+ setNewImages(next);
373
+ (_a = onFileSelectRef.current) == null ? void 0 : _a.call(onFileSelectRef, next.map((item) => item.file));
374
+ }, []);
375
+ const handleChange = useCallback(
376
+ (e) => {
377
+ if (!e.target.files) return;
378
+ processFiles(Array.from(e.target.files));
379
+ e.target.value = "";
380
+ },
381
+ [processFiles]
382
+ );
383
+ const handleDragOver = useCallback((e) => {
384
+ e.preventDefault();
385
+ e.stopPropagation();
386
+ setIsDragActive(true);
387
+ }, []);
388
+ const handleDragLeave = useCallback((e) => {
389
+ e.preventDefault();
390
+ e.stopPropagation();
391
+ if (e.currentTarget.contains(e.relatedTarget)) return;
392
+ setIsDragActive(false);
393
+ }, []);
394
+ const handleDrop = useCallback(
395
+ (e) => {
396
+ e.preventDefault();
397
+ e.stopPropagation();
398
+ setIsDragActive(false);
399
+ processFiles(Array.from(e.dataTransfer.files));
400
+ },
401
+ [processFiles]
402
+ );
403
+ return {
404
+ existingImages,
405
+ newImages,
406
+ totalCount: existingImages.length + newImages.length,
407
+ isDragActive,
408
+ inputRef,
409
+ openFilePicker: () => {
410
+ var _a;
411
+ return (_a = inputRef.current) == null ? void 0 : _a.click();
412
+ },
413
+ removeExisting,
414
+ removeNew,
415
+ dragHandlers: {
416
+ onDragOver: handleDragOver,
417
+ onDragLeave: handleDragLeave,
418
+ onDrop: handleDrop
419
+ },
420
+ inputProps: {
421
+ onChange: handleChange,
422
+ accept,
423
+ multiple: true
424
+ }
425
+ };
426
+ }
427
+
428
+ // src/components/image-uploader/image-uploader.tsx
429
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
430
+ var imageUploaderVariants = cva3("flex gap-2", {
431
+ variants: {
432
+ layout: {
433
+ /** 가로 스크롤 행 (기본값) */
434
+ row: "flex-row flex-nowrap overflow-x-auto",
435
+ /** 자동 줄바꿈 격자 */
436
+ grid: "flex-wrap"
437
+ }
438
+ },
439
+ defaultVariants: { layout: "row" }
440
+ });
441
+ function AddButton({
442
+ onClick,
443
+ disabled,
444
+ current,
445
+ max,
446
+ children
447
+ }) {
448
+ return /* @__PURE__ */ jsx5(
28
449
  "button",
29
450
  {
30
451
  type: "button",
31
- className: cx(
32
- base,
33
- sizes[size],
34
- variants[variant],
35
- fullWidth && "w-full",
36
- className
452
+ onClick,
453
+ disabled,
454
+ "aria-label": `\uC774\uBBF8\uC9C0 \uCD94\uAC00 (${current}/${max})`,
455
+ className: cn(
456
+ "shrink-0 w-20 h-20",
457
+ "flex flex-col items-center justify-center gap-1",
458
+ "rounded-lg",
459
+ "border-2 border-dashed border-border",
460
+ "bg-muted text-muted-foreground",
461
+ "cursor-pointer select-none",
462
+ "transition-colors duration-150",
463
+ "hover:bg-accent hover:text-accent-foreground hover:border-border",
464
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
465
+ "disabled:pointer-events-none disabled:opacity-50"
37
466
  ),
38
- ...rest,
39
- children: children || label
467
+ children: children != null ? children : /* @__PURE__ */ jsxs3(Fragment2, { children: [
468
+ /* @__PURE__ */ jsx5(PlusIcon, { className: "w-5 h-5" }),
469
+ /* @__PURE__ */ jsxs3("span", { className: "text-[10px] font-medium leading-none tabular-nums", children: [
470
+ current,
471
+ "/",
472
+ max
473
+ ] })
474
+ ] })
40
475
  }
41
476
  );
42
477
  }
478
+ function ImageItem({ src, alt, onRemove, disabled }) {
479
+ return /* @__PURE__ */ jsxs3("div", { className: "relative shrink-0 w-20 h-20 group/item", children: [
480
+ /* @__PURE__ */ jsx5(
481
+ "img",
482
+ {
483
+ src,
484
+ alt,
485
+ draggable: false,
486
+ className: "w-full h-full object-cover rounded-lg"
487
+ }
488
+ ),
489
+ !disabled && /* @__PURE__ */ jsx5(
490
+ "button",
491
+ {
492
+ type: "button",
493
+ onClick: onRemove,
494
+ "aria-label": `${alt} \uC0AD\uC81C`,
495
+ className: cn(
496
+ "absolute -top-1.5 -right-1.5",
497
+ "w-5 h-5 rounded-full",
498
+ "flex items-center justify-center",
499
+ "bg-foreground text-background",
500
+ "transition-all duration-150",
501
+ "opacity-0 scale-75",
502
+ "group-hover/item:opacity-100 group-hover/item:scale-100",
503
+ "group-focus-within/item:opacity-100 group-focus-within/item:scale-100",
504
+ "hover:scale-110 active:scale-90",
505
+ "focus-visible:opacity-100 focus-visible:scale-100",
506
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
507
+ ),
508
+ children: /* @__PURE__ */ jsx5(XIcon, { strokeWidth: "2.5" })
509
+ }
510
+ )
511
+ ] });
512
+ }
513
+ var ImageUploader = forwardRef3(
514
+ ({
515
+ className,
516
+ layout,
517
+ disabled,
518
+ placeholder,
519
+ initialImages,
520
+ maxImages = 4,
521
+ maxSizeMb,
522
+ accept,
523
+ onFileSelect,
524
+ onDeleteExisting,
525
+ onError
526
+ }, ref) => {
527
+ const uid = useId();
528
+ const {
529
+ existingImages,
530
+ newImages,
531
+ totalCount,
532
+ isDragActive,
533
+ inputRef,
534
+ openFilePicker,
535
+ removeExisting,
536
+ removeNew,
537
+ dragHandlers,
538
+ inputProps
539
+ } = useImageUploader({
540
+ initialImages,
541
+ maxImages,
542
+ maxSizeMb,
543
+ accept,
544
+ onFileSelect,
545
+ onDeleteExisting,
546
+ onError
547
+ });
548
+ const canAdd = !disabled && totalCount < maxImages;
549
+ return /* @__PURE__ */ jsxs3("div", { ref, className: cn("flex flex-col gap-2", className), children: [
550
+ /* @__PURE__ */ jsx5(
551
+ "div",
552
+ {
553
+ role: "status",
554
+ "aria-live": "polite",
555
+ "aria-atomic": "true",
556
+ className: "sr-only",
557
+ children: totalCount > 0 ? `\uC774\uBBF8\uC9C0 ${totalCount}\uAC1C \uC120\uD0DD\uB428. \uCD5C\uB300 ${maxImages}\uAC1C\uAE4C\uC9C0 \uCD94\uAC00\uD560 \uC218 \uC788\uC5B4\uC694.` : `\uC774\uBBF8\uC9C0\uB97C \uCD94\uAC00\uD574\uC8FC\uC138\uC694. \uCD5C\uB300 ${maxImages}\uAC1C\uAE4C\uC9C0 \uCD94\uAC00\uD560 \uC218 \uC788\uC5B4\uC694.`
558
+ }
559
+ ),
560
+ isDragActive && /* @__PURE__ */ jsx5("span", { role: "alert", className: "sr-only", children: "\uD30C\uC77C\uC744 \uC5EC\uAE30\uC5D0 \uB193\uC544\uC8FC\uC138\uC694" }),
561
+ /* @__PURE__ */ jsx5(
562
+ "input",
563
+ {
564
+ ref: inputRef,
565
+ id: `${uid}-input`,
566
+ type: "file",
567
+ tabIndex: -1,
568
+ "aria-hidden": "true",
569
+ className: "sr-only",
570
+ disabled,
571
+ ...inputProps
572
+ }
573
+ ),
574
+ /* @__PURE__ */ jsxs3(
575
+ "div",
576
+ {
577
+ role: "group",
578
+ "aria-label": `\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB354, ${totalCount}/${maxImages}\uAC1C \uC120\uD0DD\uB428`,
579
+ className: cn(
580
+ imageUploaderVariants({ layout }),
581
+ "rounded-lg p-1 -ml-1",
582
+ "transition-all duration-200",
583
+ isDragActive && "ring-2 ring-primary ring-offset-2 bg-primary/5"
584
+ ),
585
+ ...canAdd ? dragHandlers : {},
586
+ children: [
587
+ canAdd && /* @__PURE__ */ jsx5(
588
+ AddButton,
589
+ {
590
+ onClick: openFilePicker,
591
+ disabled,
592
+ current: totalCount,
593
+ max: maxImages,
594
+ children: placeholder
595
+ }
596
+ ),
597
+ sortBySequence(existingImages).map((img, index) => /* @__PURE__ */ jsx5(
598
+ ImageItem,
599
+ {
600
+ src: img.url,
601
+ alt: `\uC774\uBBF8\uC9C0 ${index + 1}`,
602
+ onRemove: () => removeExisting(img.id),
603
+ disabled
604
+ },
605
+ `existing-${img.id}`
606
+ )),
607
+ newImages.map((item, index) => /* @__PURE__ */ jsx5(
608
+ ImageItem,
609
+ {
610
+ src: item.preview,
611
+ alt: `\uC0C8 \uC774\uBBF8\uC9C0 ${existingImages.length + index + 1}`,
612
+ onRemove: () => removeNew(item.id),
613
+ disabled
614
+ },
615
+ `new-${item.id}`
616
+ ))
617
+ ]
618
+ }
619
+ )
620
+ ] });
621
+ }
622
+ );
623
+ ImageUploader.displayName = "ImageUploader";
624
+ function sortBySequence(images) {
625
+ return [...images].sort(
626
+ (a, b) => {
627
+ var _a, _b;
628
+ return ((_a = a.sequence) != null ? _a : 0) - ((_b = b.sequence) != null ? _b : 0);
629
+ }
630
+ );
631
+ }
632
+
633
+ // src/theme/provider.tsx
634
+ import {
635
+ createContext,
636
+ useEffect as useEffect2,
637
+ useState as useState2,
638
+ useMemo as useMemo2
639
+ } from "react";
640
+
641
+ // src/theme/theme-to-css-vars.ts
642
+ function themeToCssVars(theme) {
643
+ const cssVars = {};
644
+ if (theme.colors) {
645
+ Object.entries(theme.colors).forEach(([key, value]) => {
646
+ if (typeof value === "string") {
647
+ cssVars[`--color-${key}`] = value;
648
+ } else if (typeof value === "object" && value !== null) {
649
+ if ("DEFAULT" in value) {
650
+ cssVars[`--color-${key}`] = value.DEFAULT;
651
+ }
652
+ if ("foreground" in value) {
653
+ cssVars[`--color-${key}-foreground`] = value.foreground;
654
+ }
655
+ }
656
+ });
657
+ }
658
+ if (theme.radius) {
659
+ Object.entries(theme.radius).forEach(([key, value]) => {
660
+ cssVars[`--radius-${key}`] = value;
661
+ });
662
+ }
663
+ if (theme.fontSize) {
664
+ Object.entries(theme.fontSize).forEach(([key, value]) => {
665
+ if (Array.isArray(value)) {
666
+ const [size, config] = value;
667
+ cssVars[`--text-${key}`] = size;
668
+ if (config.lineHeight) {
669
+ cssVars[`--leading-${key}`] = config.lineHeight;
670
+ }
671
+ } else {
672
+ cssVars[`--text-${key}`] = value;
673
+ }
674
+ });
675
+ }
676
+ if (theme.fontFamily) {
677
+ Object.entries(theme.fontFamily).forEach(([key, value]) => {
678
+ cssVars[`--font-${key}`] = value.join(", ");
679
+ });
680
+ }
681
+ if (theme.fontWeight) {
682
+ Object.entries(theme.fontWeight).forEach(([key, value]) => {
683
+ cssVars[`--font-weight-${key}`] = value;
684
+ });
685
+ }
686
+ if (theme.shadows) {
687
+ Object.entries(theme.shadows).forEach(([key, value]) => {
688
+ cssVars[`--shadow-${key}`] = value;
689
+ });
690
+ }
691
+ if (theme.zIndex) {
692
+ Object.entries(theme.zIndex).forEach(([key, value]) => {
693
+ cssVars[`--z-${key}`] = value.toString();
694
+ });
695
+ }
696
+ if (theme.animation) {
697
+ if (theme.animation.duration) {
698
+ Object.entries(theme.animation.duration).forEach(
699
+ ([key, value]) => {
700
+ cssVars[`--duration-${key}`] = value;
701
+ }
702
+ );
703
+ }
704
+ if (theme.animation.ease) {
705
+ Object.entries(theme.animation.ease).forEach(([key, value]) => {
706
+ cssVars[`--ease-${key}`] = value;
707
+ });
708
+ }
709
+ }
710
+ if (theme.spacing) {
711
+ Object.entries(theme.spacing).forEach(([key, value]) => {
712
+ cssVars[`--spacing-${key}`] = value;
713
+ });
714
+ }
715
+ return cssVars;
716
+ }
717
+ function applyCssVars(element, cssVars) {
718
+ Object.entries(cssVars).forEach(([key, value]) => {
719
+ element.style.setProperty(key, value);
720
+ });
721
+ }
722
+ function formatCssVars(cssVars) {
723
+ return Object.entries(cssVars).map(([key, value]) => ` ${key}: ${value};`).join("\n");
724
+ }
725
+ function generateThemeCss(theme) {
726
+ const cssVars = themeToCssVars(theme);
727
+ return formatCssVars(cssVars);
728
+ }
729
+
730
+ // src/theme/provider.tsx
731
+ import { jsx as jsx6 } from "react/jsx-runtime";
732
+ var ThemeContext = createContext(void 0);
733
+ function ThemeProvider({
734
+ children,
735
+ defaultTheme = "system",
736
+ storageKey = "poodle-ui-theme",
737
+ config,
738
+ darkConfig,
739
+ customThemes = {},
740
+ enableColorSchemeSync = true
741
+ }) {
742
+ const [theme, setTheme] = useState2(() => {
743
+ if (typeof window === "undefined") {
744
+ return defaultTheme;
745
+ }
746
+ try {
747
+ return localStorage.getItem(storageKey) || defaultTheme;
748
+ } catch {
749
+ return defaultTheme;
750
+ }
751
+ });
752
+ const lightCssVars = useMemo2(
753
+ () => config ? themeToCssVars(config) : null,
754
+ [config]
755
+ );
756
+ const darkCssVars = useMemo2(
757
+ () => darkConfig ? themeToCssVars(darkConfig) : null,
758
+ [darkConfig]
759
+ );
760
+ useEffect2(() => {
761
+ var _a, _b;
762
+ const root = window.document.documentElement;
763
+ root.classList.remove(
764
+ "light",
765
+ "dark",
766
+ ...Object.keys(customThemes)
767
+ );
768
+ let actualTheme;
769
+ if (theme === "system") {
770
+ actualTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
771
+ } else {
772
+ actualTheme = theme;
773
+ }
774
+ root.classList.add(actualTheme);
775
+ if (enableColorSchemeSync) {
776
+ const isDark = actualTheme === "dark" || ((_a = customThemes[actualTheme]) == null ? void 0 : _a.dark);
777
+ root.style.colorScheme = isDark ? "dark" : "light";
778
+ }
779
+ if (actualTheme === "light" && lightCssVars) {
780
+ applyCssVars(root, lightCssVars);
781
+ } else if (actualTheme === "dark" && darkCssVars) {
782
+ applyCssVars(root, darkCssVars);
783
+ } else if ((_b = customThemes[actualTheme]) == null ? void 0 : _b.cssVars) {
784
+ Object.entries(customThemes[actualTheme].cssVars).forEach(
785
+ ([key, value2]) => {
786
+ root.style.setProperty(key, value2);
787
+ }
788
+ );
789
+ }
790
+ }, [
791
+ theme,
792
+ lightCssVars,
793
+ darkCssVars,
794
+ customThemes,
795
+ enableColorSchemeSync
796
+ ]);
797
+ useEffect2(() => {
798
+ if (theme !== "system") return;
799
+ const mediaQuery = window.matchMedia(
800
+ "(prefers-color-scheme: dark)"
801
+ );
802
+ const handler = () => {
803
+ setTheme("system");
804
+ };
805
+ mediaQuery.addEventListener("change", handler);
806
+ return () => mediaQuery.removeEventListener("change", handler);
807
+ }, [theme]);
808
+ const handleSetTheme = (newTheme) => {
809
+ try {
810
+ localStorage.setItem(storageKey, newTheme);
811
+ } catch {
812
+ }
813
+ setTheme(newTheme);
814
+ };
815
+ const value = {
816
+ theme,
817
+ setTheme: handleSetTheme
818
+ };
819
+ return /* @__PURE__ */ jsx6(ThemeContext.Provider, { value, children });
820
+ }
821
+
822
+ // src/theme/use-theme.ts
823
+ import { useContext } from "react";
824
+ function useTheme() {
825
+ const context = useContext(ThemeContext);
826
+ if (context === void 0) {
827
+ throw new Error("useTheme must be used within a ThemeProvider");
828
+ }
829
+ return context;
830
+ }
831
+
832
+ // src/theme/types.ts
833
+ function defineTheme(config) {
834
+ return config;
835
+ }
836
+
837
+ // src/theme/defaults/default.ts
838
+ var defaultLightTheme = defineTheme({
839
+ colors: {
840
+ // Page & Container colors
841
+ background: {
842
+ DEFAULT: "oklch(1 0 0)",
843
+ foreground: "oklch(0.09 0 0)"
844
+ },
845
+ muted: {
846
+ DEFAULT: "oklch(0.96 0 0)",
847
+ foreground: "oklch(0.45 0 0)"
848
+ },
849
+ accent: {
850
+ DEFAULT: "oklch(0.96 0 0)",
851
+ foreground: "oklch(0.09 0 0)"
852
+ },
853
+ card: {
854
+ DEFAULT: "oklch(1 0 0)",
855
+ foreground: "oklch(0.09 0 0)"
856
+ },
857
+ popover: {
858
+ DEFAULT: "oklch(1 0 0)",
859
+ foreground: "oklch(0.09 0 0)"
860
+ },
861
+ // Brand & Action colors
862
+ primary: {
863
+ DEFAULT: "oklch(0.09 0 0)",
864
+ foreground: "oklch(0.98 0 0)"
865
+ },
866
+ secondary: {
867
+ DEFAULT: "oklch(0.45 0 0)",
868
+ foreground: "oklch(0.98 0 0)"
869
+ },
870
+ // Semantic colors
871
+ info: {
872
+ DEFAULT: "oklch(0.55 0.15 250)",
873
+ foreground: "oklch(0.98 0 0)"
874
+ },
875
+ danger: {
876
+ DEFAULT: "oklch(0.576 0.204 27.325)",
877
+ foreground: "oklch(0.98 0 0)"
878
+ },
879
+ warning: {
880
+ DEFAULT: "oklch(0.75 0.15 75)",
881
+ foreground: "oklch(0.09 0 0)"
882
+ },
883
+ success: {
884
+ DEFAULT: "oklch(0.60 0.15 145)",
885
+ foreground: "oklch(0.98 0 0)"
886
+ },
887
+ // Border & Input colors
888
+ border: "oklch(0.90 0 0)",
889
+ input: "oklch(0.90 0 0)",
890
+ ring: "oklch(0.09 0 0)"
891
+ },
892
+ radius: {
893
+ none: "0",
894
+ sm: "0.125rem",
895
+ base: "0.25rem",
896
+ md: "0.375rem",
897
+ lg: "0.5rem",
898
+ xl: "0.75rem",
899
+ "2xl": "1rem",
900
+ "3xl": "1.5rem",
901
+ full: "9999px"
902
+ }
903
+ });
904
+ var defaultDarkTheme = defineTheme({
905
+ colors: {
906
+ // Page & Container colors
907
+ background: {
908
+ DEFAULT: "oklch(0.09 0 0)",
909
+ foreground: "oklch(0.98 0 0)"
910
+ },
911
+ muted: {
912
+ DEFAULT: "oklch(0.17 0 0)",
913
+ foreground: "oklch(0.65 0 0)"
914
+ },
915
+ accent: {
916
+ DEFAULT: "oklch(0.17 0 0)",
917
+ foreground: "oklch(0.98 0 0)"
918
+ },
919
+ card: {
920
+ DEFAULT: "oklch(0.09 0 0)",
921
+ foreground: "oklch(0.98 0 0)"
922
+ },
923
+ popover: {
924
+ DEFAULT: "oklch(0.09 0 0)",
925
+ foreground: "oklch(0.98 0 0)"
926
+ },
927
+ // Brand & Action colors
928
+ primary: {
929
+ DEFAULT: "oklch(0.98 0 0)",
930
+ foreground: "oklch(0.09 0 0)"
931
+ },
932
+ secondary: {
933
+ DEFAULT: "oklch(0.65 0 0)",
934
+ foreground: "oklch(0.09 0 0)"
935
+ },
936
+ // Semantic colors
937
+ info: {
938
+ DEFAULT: "oklch(0.65 0.15 250)",
939
+ foreground: "oklch(0.09 0 0)"
940
+ },
941
+ danger: {
942
+ DEFAULT: "oklch(0.701 0.191 29.234)",
943
+ foreground: "oklch(0.09 0 0)"
944
+ },
945
+ warning: {
946
+ DEFAULT: "oklch(0.80 0.15 75)",
947
+ foreground: "oklch(0.09 0 0)"
948
+ },
949
+ success: {
950
+ DEFAULT: "oklch(0.70 0.15 145)",
951
+ foreground: "oklch(0.09 0 0)"
952
+ },
953
+ // Border & Input colors
954
+ border: "oklch(0.27 0 0)",
955
+ input: "oklch(0.27 0 0)",
956
+ ring: "oklch(0.98 0 0)"
957
+ },
958
+ radius: {
959
+ none: "0",
960
+ sm: "0.125rem",
961
+ base: "0.25rem",
962
+ md: "0.375rem",
963
+ lg: "0.5rem",
964
+ xl: "0.75rem",
965
+ "2xl": "1rem",
966
+ "3xl": "1.5rem",
967
+ full: "9999px"
968
+ }
969
+ });
43
970
  export {
44
- Button
971
+ Button,
972
+ ImageUploader,
973
+ Input,
974
+ InputMessage,
975
+ ThemeProvider,
976
+ buttonVariants,
977
+ cn,
978
+ defaultDarkTheme,
979
+ defaultLightTheme,
980
+ defineTheme,
981
+ generateThemeCss,
982
+ imageUploaderVariants,
983
+ inputVariants,
984
+ useImageUploader,
985
+ useTheme
45
986
  };
46
987
  //# sourceMappingURL=index.mjs.map