@bigtablet/design-system 3.6.0 → 3.7.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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React$1 from 'react';
2
- import { Ref } from 'react';
2
+ import { HTMLAttributes, Ref } from 'react';
3
3
  import * as _react_spring_web from '@react-spring/web';
4
4
  import { LucideProps, LucideIcon } from 'lucide-react';
5
5
  export { LucideIcon, LucideProps } from 'lucide-react';
@@ -1775,7 +1775,7 @@ interface ImageCropperHandle {
1775
1775
  /** 배율·위치를 초기 상태(전체가 보이는 zoom=min, 중앙)로 되돌린다. */
1776
1776
  reset: () => void;
1777
1777
  }
1778
- interface ImageCropperProps {
1778
+ interface ImageCropperProps extends Omit<HTMLAttributes<HTMLDivElement>, "onError" | "children" | "dangerouslySetInnerHTML"> {
1779
1779
  /** 크롭/리셋을 호출할 imperative 핸들 (React 19 ref-as-prop). */
1780
1780
  ref?: Ref<ImageCropperHandle>;
1781
1781
  /** 크롭할 이미지. `File`/`Blob`(로컬) 또는 이미지 URL 문자열. */
@@ -1801,6 +1801,16 @@ interface ImageCropperProps {
1801
1801
  className?: string;
1802
1802
  /** 뷰포트 접근성 레이블. 기본 "이미지 위치와 배율 조정". */
1803
1803
  label?: string;
1804
+ /** 뷰포트 아래 조작 안내 문구. 기본 "드래그(또는 방향키)로 위치, 휠·슬라이더로 배율을 맞추세요." */
1805
+ hint?: string;
1806
+ /** 축소 버튼 접근성 레이블. 기본 "축소". */
1807
+ zoomOutLabel?: string;
1808
+ /** 배율 슬라이더 접근성 레이블. 기본 "배율". */
1809
+ zoomLabel?: string;
1810
+ /** 확대 버튼 접근성 레이블. 기본 "확대". */
1811
+ zoomInLabel?: string;
1812
+ /** 이동 여유가 없을 때 스크린리더로 알리는 문구. 기본 "이미지가 뷰포트를 딱 채워 이동 여유가 없습니다." */
1813
+ noPanHint?: string;
1804
1814
  }
1805
1815
  /**
1806
1816
  * 업로드 전에 이미지에서 보일 정사각 영역을 정하는 크로퍼. 뷰포트 안에서 **드래그(또는 방향키)**
@@ -1817,7 +1827,7 @@ interface ImageCropperProps {
1817
1827
  * 원격 URL 을 넘기면 `canvas.drawImage` 가 서버의 CORS(`Access-Control-Allow-Origin`)에
1818
1828
  * 의존한다 — 확실히 자르려면 로컬 `File`/`Blob` 을 권장한다.
1819
1829
  */
1820
- declare function ImageCropper({ ref, src, outputSize, outputType, quality, circular, viewportSize, minZoom, maxZoom, onReady, onError, className, label, }: ImageCropperProps): React$1.JSX.Element;
1830
+ declare function ImageCropper({ ref, src, outputSize, outputType, quality, circular, viewportSize, minZoom, maxZoom, onReady, onError, className, label, hint, zoomOutLabel, zoomLabel, zoomInLabel, noPanHint, ...rest }: ImageCropperProps): React$1.JSX.Element;
1821
1831
 
1822
1832
  interface OtpInputProps {
1823
1833
  /** OTP 자릿수 (기본값: 6) */
package/dist/index.js CHANGED
@@ -3561,12 +3561,20 @@ function ImageCropper({
3561
3561
  onReady,
3562
3562
  onError,
3563
3563
  className,
3564
- label = "\uC774\uBBF8\uC9C0 \uC704\uCE58\uC640 \uBC30\uC728 \uC870\uC815"
3564
+ label = "\uC774\uBBF8\uC9C0 \uC704\uCE58\uC640 \uBC30\uC728 \uC870\uC815",
3565
+ hint = "\uB4DC\uB798\uADF8(\uB610\uB294 \uBC29\uD5A5\uD0A4)\uB85C \uC704\uCE58, \uD720\xB7\uC2AC\uB77C\uC774\uB354\uB85C \uBC30\uC728\uC744 \uB9DE\uCD94\uC138\uC694.",
3566
+ zoomOutLabel = "\uCD95\uC18C",
3567
+ zoomLabel = "\uBC30\uC728",
3568
+ zoomInLabel = "\uD655\uB300",
3569
+ noPanHint = "\uC774\uBBF8\uC9C0\uAC00 \uBDF0\uD3EC\uD2B8\uB97C \uB531 \uCC44\uC6CC \uC774\uB3D9 \uC5EC\uC720\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
3570
+ ...rest
3565
3571
  }) {
3566
3572
  const imageRef = useRef(null);
3573
+ const viewportRef = useRef(null);
3567
3574
  const dragRef = useRef(
3568
3575
  null
3569
3576
  );
3577
+ const hintId = useId();
3570
3578
  const [previewUrl, setPreviewUrl] = useState("");
3571
3579
  const [srcType, setSrcType] = useState("");
3572
3580
  useEffect(() => {
@@ -3582,11 +3590,13 @@ function ImageCropper({
3582
3590
  }, [src]);
3583
3591
  const [imageSize, setImageSize] = useState(null);
3584
3592
  const [zoom, setZoom] = useState(minZoom);
3593
+ const zoomRef = useRef(zoom);
3585
3594
  const [offset, setOffset] = useState({ x: 0, y: 0 });
3586
3595
  const [dragging, setDragging] = useState(false);
3587
3596
  useEffect(() => {
3588
3597
  setImageSize(null);
3589
3598
  setZoom(minZoom);
3599
+ zoomRef.current = minZoom;
3590
3600
  setOffset({ x: 0, y: 0 });
3591
3601
  }, [src, minZoom]);
3592
3602
  const handleImageLoad = (event) => {
@@ -3601,6 +3611,7 @@ function ImageCropper({
3601
3611
  (next) => {
3602
3612
  const clampedZoom = Math.min(maxZoom, Math.max(minZoom, next));
3603
3613
  setZoom(clampedZoom);
3614
+ zoomRef.current = clampedZoom;
3604
3615
  if (imageSize) {
3605
3616
  setOffset((prev) => clampCropOffset(prev, imageSize, viewportSize, clampedZoom));
3606
3617
  }
@@ -3637,11 +3648,16 @@ function ImageCropper({
3637
3648
  setDragging(false);
3638
3649
  }
3639
3650
  };
3640
- const handleWheel = (event) => {
3641
- if (!imageSize) return;
3642
- event.preventDefault();
3643
- applyZoom(zoom - event.deltaY * WHEEL_ZOOM_FACTOR * zoom);
3644
- };
3651
+ useEffect(() => {
3652
+ const viewport = viewportRef.current;
3653
+ if (!viewport || !imageSize) return;
3654
+ const handleWheel = (event) => {
3655
+ event.preventDefault();
3656
+ applyZoom(zoomRef.current - event.deltaY * WHEEL_ZOOM_FACTOR * zoomRef.current);
3657
+ };
3658
+ viewport.addEventListener("wheel", handleWheel, { passive: false });
3659
+ return () => viewport.removeEventListener("wheel", handleWheel);
3660
+ }, [imageSize, applyZoom]);
3645
3661
  const handleKeyDown2 = (event) => {
3646
3662
  if (!imageSize) return;
3647
3663
  switch (event.key) {
@@ -3675,6 +3691,7 @@ function ImageCropper({
3675
3691
  () => ({
3676
3692
  reset: () => {
3677
3693
  setZoom(minZoom);
3694
+ zoomRef.current = minZoom;
3678
3695
  setOffset({ x: 0, y: 0 });
3679
3696
  },
3680
3697
  crop: () => new Promise((resolve, reject) => {
@@ -3722,92 +3739,95 @@ function ImageCropper({
3722
3739
  height: imageSize.height * scale,
3723
3740
  transform: `translate(calc(-50% + ${offset.x}px), calc(-50% + ${offset.y}px))`
3724
3741
  } : { visibility: "hidden" };
3725
- return /* @__PURE__ */ jsxs("div", { className: cn("image_cropper", className), children: [
3726
- /* @__PURE__ */ jsxs(
3727
- "div",
3728
- {
3729
- className: cn("image_cropper_viewport", dragging && "image_cropper_viewport_dragging"),
3730
- style: viewportStyle,
3731
- role: "group",
3732
- "aria-label": label,
3733
- "aria-describedby": "image_cropper_hint",
3734
- tabIndex: imageSize ? 0 : -1,
3735
- onPointerDown: handlePointerDown,
3736
- onPointerMove: handlePointerMove,
3737
- onPointerUp: handlePointerEnd,
3738
- onPointerCancel: handlePointerEnd,
3739
- onWheel: handleWheel,
3740
- onKeyDown: handleKeyDown2,
3741
- children: [
3742
- /* @__PURE__ */ jsx(
3743
- "img",
3744
- {
3745
- ref: imageRef,
3746
- src: previewUrl || void 0,
3747
- alt: "",
3748
- className: "image_cropper_image",
3749
- draggable: false,
3750
- crossOrigin: typeof src === "string" ? "anonymous" : void 0,
3751
- onLoad: handleImageLoad,
3752
- onError,
3753
- style: imageStyle
3754
- }
3755
- ),
3756
- /* @__PURE__ */ jsx(
3757
- "div",
3758
- {
3759
- className: cn(
3760
- "image_cropper_mask",
3761
- circular && "image_cropper_mask_circular",
3762
- dragging && "image_cropper_mask_active"
3763
- ),
3764
- "aria-hidden": "true"
3765
- }
3766
- )
3767
- ]
3768
- }
3769
- ),
3770
- /* @__PURE__ */ jsx("p", { id: "image_cropper_hint", className: "image_cropper_hint", children: "\uB4DC\uB798\uADF8(\uB610\uB294 \uBC29\uD5A5\uD0A4)\uB85C \uC704\uCE58, \uD720\xB7\uC2AC\uB77C\uC774\uB354\uB85C \uBC30\uC728\uC744 \uB9DE\uCD94\uC138\uC694." }),
3771
- /* @__PURE__ */ jsxs("div", { className: "image_cropper_zoom", children: [
3772
- /* @__PURE__ */ jsx(
3773
- "button",
3774
- {
3775
- type: "button",
3776
- className: "image_cropper_zoom_button",
3777
- "aria-label": "\uCD95\uC18C",
3778
- disabled: !imageSize || zoom <= minZoom,
3779
- onClick: () => applyZoom(zoom - ZOOM_KEY_STEP),
3780
- children: "\u2212"
3781
- }
3782
- ),
3783
- /* @__PURE__ */ jsx(
3784
- "input",
3742
+ return (
3743
+ // className 위에서 별도 구조분해되어 rest 에 없으므로 spread 순서와 무관하게 충돌하지 않는다.
3744
+ /* @__PURE__ */ jsxs("div", { ...rest, className: cn("image_cropper", className), children: [
3745
+ /* @__PURE__ */ jsxs(
3746
+ "div",
3785
3747
  {
3786
- type: "range",
3787
- className: "image_cropper_zoom_slider",
3788
- "aria-label": "\uBC30\uC728",
3789
- min: minZoom,
3790
- max: maxZoom,
3791
- step: ZOOM_STEP,
3792
- value: zoom,
3793
- disabled: !imageSize,
3794
- onChange: (event) => applyZoom(Number(event.target.value))
3748
+ ref: viewportRef,
3749
+ className: cn("image_cropper_viewport", dragging && "image_cropper_viewport_dragging"),
3750
+ style: viewportStyle,
3751
+ role: "group",
3752
+ "aria-label": label,
3753
+ "aria-describedby": hintId,
3754
+ tabIndex: imageSize ? 0 : -1,
3755
+ onPointerDown: handlePointerDown,
3756
+ onPointerMove: handlePointerMove,
3757
+ onPointerUp: handlePointerEnd,
3758
+ onPointerCancel: handlePointerEnd,
3759
+ onKeyDown: handleKeyDown2,
3760
+ children: [
3761
+ /* @__PURE__ */ jsx(
3762
+ "img",
3763
+ {
3764
+ ref: imageRef,
3765
+ src: previewUrl || void 0,
3766
+ alt: "",
3767
+ className: "image_cropper_image",
3768
+ draggable: false,
3769
+ crossOrigin: typeof src === "string" ? "anonymous" : void 0,
3770
+ onLoad: handleImageLoad,
3771
+ onError,
3772
+ style: imageStyle
3773
+ }
3774
+ ),
3775
+ /* @__PURE__ */ jsx(
3776
+ "div",
3777
+ {
3778
+ className: cn(
3779
+ "image_cropper_mask",
3780
+ circular && "image_cropper_mask_circular",
3781
+ dragging && "image_cropper_mask_active"
3782
+ ),
3783
+ "aria-hidden": "true"
3784
+ }
3785
+ )
3786
+ ]
3795
3787
  }
3796
3788
  ),
3797
- /* @__PURE__ */ jsx(
3798
- "button",
3799
- {
3800
- type: "button",
3801
- className: "image_cropper_zoom_button",
3802
- "aria-label": "\uD655\uB300",
3803
- disabled: !imageSize || zoom >= maxZoom,
3804
- onClick: () => applyZoom(zoom + ZOOM_KEY_STEP),
3805
- children: "\uFF0B"
3806
- }
3807
- )
3808
- ] }),
3809
- /* @__PURE__ */ jsx("span", { className: "image_cropper_sr_only", "aria-live": "polite", children: imageSize && !canPan ? "\uC774\uBBF8\uC9C0\uAC00 \uBDF0\uD3EC\uD2B8\uB97C \uB531 \uCC44\uC6CC \uC774\uB3D9 \uC5EC\uC720\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." : "" })
3810
- ] });
3789
+ /* @__PURE__ */ jsx("p", { id: hintId, className: "image_cropper_hint", children: hint }),
3790
+ /* @__PURE__ */ jsxs("div", { className: "image_cropper_zoom", children: [
3791
+ /* @__PURE__ */ jsx(
3792
+ "button",
3793
+ {
3794
+ type: "button",
3795
+ className: "image_cropper_zoom_button",
3796
+ "aria-label": zoomOutLabel,
3797
+ disabled: !imageSize || zoom <= minZoom,
3798
+ onClick: () => applyZoom(zoom - ZOOM_KEY_STEP),
3799
+ children: "\u2212"
3800
+ }
3801
+ ),
3802
+ /* @__PURE__ */ jsx(
3803
+ "input",
3804
+ {
3805
+ type: "range",
3806
+ className: "image_cropper_zoom_slider",
3807
+ "aria-label": zoomLabel,
3808
+ min: minZoom,
3809
+ max: maxZoom,
3810
+ step: ZOOM_STEP,
3811
+ value: zoom,
3812
+ disabled: !imageSize,
3813
+ onChange: (event) => applyZoom(Number(event.target.value))
3814
+ }
3815
+ ),
3816
+ /* @__PURE__ */ jsx(
3817
+ "button",
3818
+ {
3819
+ type: "button",
3820
+ className: "image_cropper_zoom_button",
3821
+ "aria-label": zoomInLabel,
3822
+ disabled: !imageSize || zoom >= maxZoom,
3823
+ onClick: () => applyZoom(zoom + ZOOM_KEY_STEP),
3824
+ children: "\uFF0B"
3825
+ }
3826
+ )
3827
+ ] }),
3828
+ /* @__PURE__ */ jsx("span", { className: "image_cropper_sr_only", "aria-live": "polite", children: imageSize && !canPan ? noPanHint : "" })
3829
+ ] })
3830
+ );
3811
3831
  }
3812
3832
  var OtpInput = ({
3813
3833
  length = 6,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigtablet/design-system",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Bigtablet Design System UI Components",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",